JSON
Now that you know how to parse command-line arguments using
process.argv
and
yargs, you've solved the first piece to the puzzle for the
notes
application. Now,
how do we get that unique input from the user? The second piece to the puzzle is
to solve how we store this information.
When someone adds a new note, we want to save it somewhere, preferably on the
filesystem. So the next time they try to fetch, remove, or read that note, they
actually get the note back. To do this, we'll need to introduce something called
JSON. If you're already familiar with JSON, you probably know it is super
popular. It stands for JavaScript Object Notation, and it's a way to represent
JavaScript arrays and objects using a string. Now, why would you ever want to
do that?
Well, you might want to do that because strings are just text, and that's pretty
much supported anywhere. I can save JSON to a text file, and then I can read it
later, parse it back into a JavaScript array or object, and do something with it.
This is exactly what we'll take a look at in this section.
To explore JSON and how it works, let's go ahead and make a new folder inside
our project called
playground
.
Throughout the book, I'll create the
playground
folders and various
projects, which store simple one-off files that aren't a part of the
bigger application; they're just a way to explore a new feature or
learn a new concept.
In the
playground
folder, we'll make a file called
json.js
, this is where we can explore
how JSON works. To get started, let's make a very simple object.
Converting objects into strings
Let's first make a variable called
obj
, setting it equal to an object. On this object,
we'll just define one property,
name
, and set it equal to your first name; I'll set this
one equal to
Andrew
, as shown here:
var obj = { name:
'Andrew' };
Now, let's assume that we want to take this object and work on it. Let's say we
want to, for example, send it between servers as a string and save it to a text file.
To do this, we'll need to call one JSON method.
Let's take a moment to define a variable to store the result,
stringObj
, and we'll set it
equal to
JSON.stringify
, as shown here: var stringObj = JSON.stringify(obj);
The
JSON.stringify
method takes your object, in this case, the
obj
variable, and returns
the JSON-stringified version. This means that the result stored in
stringObj
is actually
a string. It's no longer an object, and we can take a look at that using console.log. I'll
use
console.log
twice. First up, we'll use the
typeof
operator to print the type of the string
object to make sure that it actually is a string. Since
typeof
is an operator, it gets
typed in lowercase, there is no camel casing. Then, you pass in the variable whose
type you want to check. Next up, we can use
console.log
to print the contents of the
string itself, printing out the
stringObj
variable, as shown here:
console.log(typeof stringObj); console.log(stringObj);
What we've done here is we've taken an object, converted it into a JSON string,
and printed it onto the screen. Over in Terminal, I'll navigate into the
playground
folder using the following command:
For now, it doesn't matter where you run the command, but in future
it will matter when we are in the
playground
folder, so take a
moment to navigate into it.
We can now use
node
to run our
json.js
file. When we run the file, we see two things:
As shown in the preceding code output, first, we will get our type, which is a
string, and this is great, because remember, JSON is a string. Next, we will get
our object, which looks pretty similar to a JavaScript object, but there are a few
differences. These differences are as follows:
First up, your JSON will have its attribute names automatically wrapped in
double quotes. This is a requirement of the JSON syntax.
Next up, you'll notice your strings are also wrapped in double quotes as
opposed to single quotes.
Now, JSON doesn't just support string values, you can use an array, a Boolean, a
number, or anything else. All of those types are perfectly valid inside of your
JSON. In this case, we have a very simple example where we have a
name
property
and it's set to
"Andrew"
.
This is the process of taking an object and converting it into a string. Next up,
we'll define a string and convert that into an object we can actually use in our app.
Defining a string and using in app
as an object
Let's get started by making a variable called
personString
, and we'll to set it equal to a
string using single quotes since JSON uses double quotes inside of itself, as
shown here:
var personString = '';
Then we'll define our JSON in the quotes. We'll start by opening and closing some
curly braces. We'll use double quotes to create our first attribute, which we'll call
name
, and we'll set that attribute equal to
Andrew
. This means that after the closing
quote, we'll add
:
; then we'll open and close double quotes again and type the
value
Andrew
, as shown here: var personString = '{"name": "Andrew"}';
Next up, we can add another property. After the value,
Andrew
, I'll create another
property after the comma, called
age
, which will be set equal to a number. I can
use my colon and then define the number without the quotes, in this case,
25
: var
personString = '{"name": "Andrew","age": 25}';
You can go ahead and use your name and your age, obviously, but make sure the
rest looks identical to what you see here.
Now, let's say we get the earlier-defined JSON from a server or we grab it from
a text file. Currently, it's useless; if we want to get the
name
value, there is no good
way to do that because we're using a string, so
personString.name
doesn't exist.
What we need to do is take the string and convert it back into an object.
Converting a string back to an
object
To convert the string back to object, we'll use the opposite of
JSON.stringify
, which is
JSON.parse
. Let's make a variable to store the result. I'll create a
person
variable and it
will be set equal to
JSON.parse
, passing in as the one and only argument the string
you want to parse, in this case, the
person
string, which we defined earlier: var person =
JSON.parse(personString);
Now, this variable takes your JSON and converts it from a string back into its
original form, which could be an array or an object. In our case, it converts it back
into an object, and we have the
person
variable as an object, as shown in the
preceding code. Also, we can prove that it's an object using the
typeof
operator. I'll
use
console.log
twice, just like we did previously.
First up, we'll print
typeof person
, and then we'll print the actual
person
variable,
console.log(person):
console.log(typeof person); console.log(person);
With this in place, we can now rerun the command in Terminal; I'll actually start
nodemon and pass in json.js: nodemon json.js
As shown in the following code output, you can now see that we're working with
an object, which is great, and we have our regular object:
We know that
Andrew
is an object because it's not wrapped in double quotes; the
values don't have any quotes, and we use single quotes for
Andrew
, which is valid
in JavaScript, but it's not valid in JSON.
This is the entire process of taking an object, converting it to a string, and then
taking the string and converting it back into the object, and this is exactly what
we'll do in the
notes
app. The only difference is that we'll be taking the following
string and storing it in a file, then later on, we'll be reading that string from the
file using
JSON.parse
to convert it back to an object, as shown in the following code
block:
// var obj = {
// name: 'Andrew'
// };
// var stringObj = JSON.stringify(obj);
// console.log(typeof stringObj); //
console.log(stringObj);
var personString = '{"name": "Andrew","age": 25}'; var person =
JSON.parse{personString}; console.log(typeof person);
console.log(person);
Storing the string in a file
With the basics in place, let's take it just one step further, that is, by storing the
string in a file. Then, we want to read the contents of that file back by using the fs
module and printing some properties from it. This means that we'll need to
convert the string that we get back from
fs.readfilesync
into an object using JSON.parse.
Writing the file in the playground
folder
Let's go ahead and comment out all the code we have so far and start with a clean
slate. First up, let's go ahead and load in the
fs
module. The
const
variable fs will be
set equal to
require
, and we'll pass the
fs
module that we've used in the past, as shown
here:
// var obj = {
// name: 'Andrew'
// };
// var stringObj = JSON.stringify(obj);
// console.log(typeof stringObj);
// console.log(stringObj);
// var personString = '{"name": "Andrew","age": 25}';
// var person = JSON.parse(personString);
// console.log(typeof person); //
console.log(person); const fs =
require('fs');
The next thing we'll do is define the object. This object will be stored inside of
our file, and then will be read back and parsed. This object will be a variable
called
originalNote
, and we'll call it
originalNote
because later on, we'll load it back in and
call that variable
Note
.
Now,
originalNote
will be a regular JavaScript object with two properties. We'll have
the
title
property, which we'll set equal to
Some title
, and the
body
property, which we
will set equal to
Some body
, as shown here:
var originalNote = { title: 'Some
title', body: 'Some body'
};
The next step that you will need to do is take the original note and create a variable
called
originalNoteString
, and set that variable equal to the JSON value of the object we
defined earlier. This means that you'll need to use one of the two JSON methods
we used previously in this section.
Now, once you have that
originalNoteString
variable, we can write a file to the
filesystem. I'll write that line for you, fs.writeFileSync. The writeFileSync method, which we
used before, takes two arguments. One will be the filename, and since we're using
JSON, it's important to use the JSON file extension. I'll call this file notes.json. The
other arguments will be text content,
originalNoteString
, which is not yet defined, as
shown in this code block:
// originalNoteString fs.writeFileSync('notes.json', originalNoteString);
This is the first step to the process; this is how we'll write that file into the playground
folder. The next step to the process will be to read out the contents, parse it using
the JSON method earlier, and print one of the properties to the screen to make
sure that it's an object. In this case, we'll print the title.
Reading out the content in the file
The first step to print the title is to use a method we haven't used yet. We'll use
the
read
method available on the filesystem module to read the contents. Let's make
a variable called
noteString
. The
noteString
variable will be set equal to fs.readFileSync.
Now,
readFileSync
is similar to
writeFileSync
except that it doesn't take the text content,
since it's getting the text content back for you. In this case, we'll just specify the
first argument, which is the filename,
notes.JSON
: var noteString = fs.readFileSync('notes.json');
Now that we have the string, it will be your job to take that string, use one of the
preceding methods, and convert it back into an object. You can call that variable
note. Next up, the only thing left to do is to test whether things are working as
expected, by printing with the help of
console.log(typeof note)
. Then, below this, we'll use
console.log to print the title, note.title:
// note
console.log(typeof note); console.log(note.title);
Now, over in Terminal, you can see (refer to the following screenshot) that I have
saved the file in a broken state and it crashed, and that's expected when you're
using
nodemon
:
To resolve this, the first thing I'll do is fill out the
originalNoteString
variable, which we
had commented out earlier. It will now be a variable called originalNoteString, and we'll
set it equal to the return value from
JSON.stringify
.
Now, we know
JSON.stringify
takes our regular object and it converts the object into a
string. In this case, we'll take the
originalNote
object and convert it into a string. The
next line, which we already have filled out, will save that JSON value into the
notes.JSON
file. Then we will read that value out: var originalNoteString = JSON.stringify(originalNote);
The next step will be to create the
note
variable. The
note
variable will be set equal
to JSON.parse.
The
JSON.parse
method takes the string JSON and converts it back into a regular
JavaScript object or array, depending on whatever you save. Here we will pass in
noteString, which we'll get from the file: var note = JSON.parse(noteString);
With this in place, we are now done. When I save this file,
nodemon
will
automatically restart and we would expect to not see an error. Instead, we expect
that we'll see the object type as well as the note title. Right inside Terminal, we
have object and Some title printing to the screen:
With this in place, we've successfully completed the challenge. This is exactly
how we will save our notes.
When someone adds a new note, we'll use the following code to save it:
var originalNote = { title: 'Some
title', body: 'Some body'
};
var originalNoteString = JSON.stringify(originalNote); fs.writeFileSync('notes.json', originalNoteString);
When someone wants to read their note, we'll use the following code to read it:
var noteString = fs.readFileSync('notes.json'); var note =
JSON.parse(noteString); console.log(typeof note);
console.log(note.title);
Now, what if someone wants to add a note? This will require us to first read all
of the notes, then modify the notes array, and then use the code (refer to the
previous code block) to save the new array back into the filesystem.
If you open up that
notes.JSON
file, you can see right here that we have our JSON
code inside the file:
.json is actually a file format that's supported by most text editors, so I actually
already have some nice syntax highlighting built in. Now, in the next section,
we'll be filling out the
addNote
function using the exact same logic that we just used
inside of this section.
Adding and saving notes
In the previous section, you learned how to work with JSON inside Node.js, and
this is the exact format we'll be using for the
notes.js
application. When you first run
a command, we'll load in all the notes that might already exist. Then we'll run the
command, whether it's adding, removing, or reading notes. Finally, if we've
updated the array, like we will when we add and remove notes, we'll save those
new notes back into the JSON file.
Now, this will all happen inside of the
addNote
function, which we defined in the
notes.js application, and we already wired up this function. In earlier sections, we
ran the app
add
command, and this function executed with the
title
and
body
arguments.
Adding notes
To get started with adding notes, the first thing we'll do is create a variable called
notes, and for the moment, we'll set it equal to an empty array, just as in the
following, using our square brackets:
var addNote = (title, body) => { var notes =
[]; };
Now that we have the empty array, we can go ahead and make a variable called
note, which is the individual note. This will represent the new note:
var addNote = (title, body) => { var notes =
[]; var note = {
}
};
On that note, we'll have the two properties: a
title
and a
body
. Now,
title
can be set
equal to the
title
variable, but, as we know, inside ES6, we can simply remove it
when both values are the same; so we'll add
title
and
body
as shown here:
var addNote = (title, body) => { var notes =
[]; var note = { title, body
};
};
Now we have the
note
and the
notes
array.
Adding notes to the notes array
The next step in the process of adding notes will be to add the
note
to the
notes
array.
The
notes.push
method will let us do just that. The
push
method on an array lets you
pass in an item, which gets added to the end of the array, and in this case, we'll
pass in the
note
object. So we have an empty array, and we add our one item, as
shown in the following code; next, we push it in, which means that we have an
array with one item:
var addNote = (title, body) => { var notes =
[]; var note = { title, body };
notes.push(note);
};
The next step in the process will be to update the file. Now, we don't have a
file in place, but we can load an
fs
function and start creating the file.
Up above the
addNote
function, let's load in the
fs
module. I'll create a
const
variable
called
fs
and set it equal to the return result from
require
, and we'll require the
fs
module, which is a core node module, so there's no need to install it using NPM:
const fs = require('fs');
With this in place, we can take advantage of
fs
inside the
addNote
function.
Right after we push our item on to the
notes
array, we'll call
fs.writeFileSync
, which we've
used before. We know we need to pass in two things: the file name and the content
we want to save. For the file, I'll call,
notes-data.JSON
, and then we'll pass in the content
to save, which in this case will be the
stringify
notes array, which means we can call
JSON.stringify
passing in
notes
:
notes.push(note); fs.writeFileSync('notes-data.json', JSON.stringify(notes));
We could have broken
JSON.stringfy(notes)
out into its own variable
and referenced the variable in the above statement, but since we'll
only be using it in one place, I find this is the better solution.
At this point, when we add a new note, it will update the
notes-data.JSON
file, which
will be created on the machine since it does not exist, and the note will sit inside
it. Now, it's important to note that currently every time you add a new note, it will
wipe all existing ones because we never load in the existing ones, but we can get
started testing that this note works as expected.
I'll save the file, and over inside of Terminal, we can run this file using
node
app.js.
Since we want to add a
note
, we will be using that
add
command which we set up,
then we'll specify our title and our body. The
title
flag can get set equal to
secret
, and
for the
body
flag, I'll set it equal to the
Some body here
string, as shown here: node app.js add
--title=secret --body="Some body here"
Now, when we run this command from Terminal, we'll see what we'd expect:
As shown in the preceding screenshot, we see a couple of the file commands we
added: we see that the
add
command was executed, and we have our Yargs
arguments. The title and body arguments also show up. Inside Atom, we also see
that we have a new
notes-data.json
file, and in the following screenshot, we have our
note, with the
secret
title and the
Some body here
body:
This is the first step in wiring up that
addNote
function. We have an existing
notes
file
and we do want to take advantage of these notes. If notes already exist, we don't
want to simply wipe them every time someone adds a new note. This means that
in
notes.js
, earlier at the beginning of the
addNote
function, we'll fetch those notes.
Fetching new notes
I'll add code for fetching new notes where I define the
notes
and
note
variables. As
shown in the following code, we'll use
fs.readFileSync
, which we've already explored.
This will take the filename, in our case,
notes-data.JSON
. Now, we will want to store
the return value from
readFileSync
on a variable; I'll call that variable, notesString: var
notesString = fs.readFileSync('notes-data.json');
Since this is the string version, we haven't passed it through the
JSON.parse
method.
So, I can set
notes
(the variable we defined earlier in
addNote
function) equal to the
return value from the
JSON.parse
method. Then
JSON.parse
will take the string from the
file we read and it will parse it into an array; we could pass in notesString just like
this: notes = JSON.parse(notesString);
With this in place, adding a new note is no longer going to remove all of the notes
that were already there.
Over in Terminal, I'll use the up arrow key to load in the last command, and I'll
navigate over to the
title
flag and change it to
secret2
and rerun the command: node
app.js add --title=secret2 --body="Some body here"
In Atom, this time you can see we now have two notes inside of our file:
We have an array with two objects; the first one has the title of
secret
and the second
one has the title of
secret2
, which is brilliant!
Trying and catching code block
Now, if the
notes-data.json
file does not exist, which it won't when the user first runs
the command, the program will crash, as shown in the following code output. We
can prove this by simply rerunning the last command after deleting the note-data.JSON
file:
Right here, you can see we're actually getting a JavaScript error, no such file or
directory; it's trying to open up the
notes-data.JSON
file, but without much success. To
fix this, we'll use a
try
-
catch
statement from JavaScript, which hopefully you've seen
in the past. To brush up this, let's go over it really quick.
To create a
try
-
catch
statement, all you do is you type
try
, which is a reserved
keyword, and then you open and close a set of curly braces. Inside the curly braces
is the code that will run. This is the code that may or may not throw an error.
Next, you'll specify the
catch
block. Now, the
catch
block will take an argument, an
error argument, and it also has a code block that runs:
try{
} catch (e) {
}
This code will run if and only if one of your errors in
try
actually occurs. So, if we
load the file using
readFileSync
and the file exists, that's fine,
catch
block will never run.
If it fails,
catch
block will run and we can do something to recover from that error.
With this in place, all we'll do is move the
noteString
variable and the
JSON.parse
statements into
try
, as shown here:
try{ var notesString = fs.readFileSync('notes-data.json'); notes =
JSON.parse(notesString); } catch (e) {
}
That's it; nothing else needs to happen. We don't need to put any code in
catch
,
although you do need to define the
catch
block. Now, let's take a look at what
happens when we run the whole code.
The first thing that happens is that we create our static variables—nothing special
there—then we try to load in the file. If the
notesString
function fails, that is fine
because we already defined
notes
to be an empty array. If the file doesn't exist and
it fails, then we probably want an empty array for
notes
anyways, because clearly
there are no
notes
, and there's no file.
Next up, we'll parse that data into notes. There is a chance that this will fail if
there's invalid data in the
notes-data.JSON
file, so the two lines can have problems. By
putting them in
try
-
catch
, we're basically guaranteeing that the program isn't going
to work unexpectedly, whether the file does or doesn't exist, but it contains
corrupted data.
With this in place, we can now save
notes
and rerun that previous command. Note
that I do not have the
notes-data
file in place. When I run the command, we don't see
any errors, everything seems to run as expected:
When you now visit Atom, you can see that the
notes-data
file does indeed exist, and
the data inside it looks great:
This is all we need to do to fetch the notes, update the notes with the new note,
and finally save the notes to the screen.
Now, there is still a slight problem with
addNote
. Currently,
addNote
allows for
duplicate titles; I could already have a note in the JSON file with the title of secret.
I can come along and try to add a new note with the title of
secret
and it will not
throw an error. What I'd like to do is to make the title unique, so that if there's
already a note with that title, it will throw an error, letting you know that you need
to create a note with a different title.
Making the title unique
The first step to make the title unique will be to loop through all of the notes after
we load them in and check whether there are any duplicates. If there are
duplicates, we'll not call the following two lines:
notes.push(note); fs.writeFileSync('notes-data.json', JSON.stringify(notes));
If there are no duplicates then it's fine, we will call both of the lines shown in the
preceding code block, updating the
notes-data
file.
Now, we'll be refactoring this function down the line. Things are getting a little
wonky and a little out of control, but for the moment, we can add this functionality
right into the function. Let's go ahead and make a variable called duplicateNotes.
The
duplicateNotes
variable will eventually store an array with all of the notes that
already exist inside the
notes
array that have the title of the note you're trying to
create. Now, this means that if the
duplicateNotes
array has any items, that's bad. This
means that the note already exists and we should not add the note. The duplicateNotes
variable will get set equal to a call to
notes
, which is our array of notes.filter: var duplicateNotes
= notes.filter();
The
filter
method is an array method that takes a callback. We'll use an arrow
function, and that callback will get called with the argument. In this case, it will
be the singular version; if I have an array of notes, it will be called with an
individual note:
var duplicateNotes = notes.filter((note) => {
});
This function gets called once for every item in the array, and you have the
opportunity to return either true or false. If you return true, it will keep that item
in the array, which will eventually get saved into
duplicateNotes
. If you return false,
the new array it generates will not have that item inside
duplicateNotes
variable. All
we want to do is to return true if the titles match, which means that we can return
note.title === title
, as shown here:
var duplicateNotes = notes.filter((note) => { return note.title ===
title;
});
If the titles are equal, then the preceding
return
statement will result as true and the
item will be kept in the array, which means that there are duplicate notes. If the
titles are not equal, which is most likely the case, the statement will result as false,
which means that there are no duplicate notes. Now, we can simplify this a little
more using arrow functions.
Arrow functions actually allow you to remove the curly braces if
you only have one statement.
I'll use the arrow function, as shown here: var duplicateNotes =
notes.filter((note) => note.title === title);
Here, I have deleted everything except
note.title === title
and added this in front of the
arrow function syntax.
This is perfectly valid using ES6 arrow functions. You have your arguments on
the left, the arrow, and on the right, you have one expression. The expression
doesn't take a semicolon and it's automatically returned as the function result.
This means that the code we have here is identical to the code we had earlier, only
it's much simpler and it only takes up one line.
Now that we have this in place, we can go ahead and check the length of the
duplicateNotes variable. If the length of
duplicateNotes
is greater than
0
, this means that we
don't want to save the note because a note already exists with that title. If it is
0
,
we'll save the note.
if(duplicateNotes.length === 0) {
}
Here, inside the
if
condition, we're comparing the notes length with the number
zero. If they are equal, then we do want to push the note onto the
notes
array and
save the file. I'll cut the following two lines:
notes.push(note); fs.writeFileSync('notes-data.json', JSON.stringify(notes));
Let's paste them right inside of the
if
statement, as shown here:
if(duplicateNotes.length === 0) { notes.push(note); fs.writeFileSync('notes-data.json',
JSON.stringify(notes)); }
If they're not equal, that's okay too; in that case we'll do nothing.
With this in place, we can now save our file and test this functionality out. We
have our
notes-data.json
file, and this file already has a note with a title of
secret2
.
You're in Terminal, so we'll head back into our JSON file. You can see right here
that we still just have one note:
Let's rerun the previous command to try to add a new note with that same
title:
Now all the titles inside of our application will be unique, so we can use these
titles to fetch and delete notes.
Let's go ahead and test that other notes can still be added. I'll change the
title
flag
from
secret2
to
secret
, and run that command: node app.js add --title=secret --body="Some body here"
Inside our
notes-data
file, you can see both notes show up:
As I mentioned earlier, next we will be doing some refactoring, since the code
that loads the file, and the code that saves the file, will both be used in most of
the functions we have defined and/or will define (that is, the
getAll
,
getNote
and
removeNote functions).
Refactoring
In the previous section, you created the
addNote
function, which works well. It starts
by creating some static variables, then we fetch any existing notes, we check for
duplicates, and if there are none, we push it onto the list, and then we save the
data back into the filesystem.
The only problem is that we'll be doing a lot of these steps over and over again
for every method. For example, with
getAll
, the idea is to fetch all of the notes, and
send them back to
app.js
so it can print them to the screen for the user. The first
thing we'll to do inside of the
getAll
statement is have the same code; we'll have our
try
-
catch
block to fetch the existing notes.
Now, this is a problem because we'll be repeating code throughout the application.
It will be best to break out the fetching of notes and the saving of notes into
separate functions that we can call in multiple locations.
Moving functionality into
individual functions
To resolve the problem, I'd like to get started by creating two new functions:
fetchNotes saveNotes
The first function,
fetchNotes
, will be an arrow function, and it will not to take any
arguments since it will be fetching notes from the filesystem, as shown here:
var fetchNotes = () => {
};
The second function,
saveNotes
, will need to take an argument. It will need to take
the
notes
array you want to save to the filesystem. We'll set it equal to an arrow
function, and then we'll provide our argument, which I will name
notes
, as shown
here:
var saveNotes = (notes) => {
};
Now that we have these two functions, we can go ahead and start moving some
of the functionality from
addNote
up into the individual functions.
Working with fetchNotes
First up, let's do
fetchNotes
, which will need the following
try
-
catch
block.
I'll actually cut it out of
addNote
and paste it in the
fetchNotes
function, as shown here:
var fetchNotes = () => { try{ var notesString = fs.readFileSync('notes-data.json');
notes = JSON.parse(notesString); } catch (e) {
}
};
This alone is not enough, because currently we don't return anything from the
function. What we want to do is to return the notes. This means that instead of
saving the result from
JSON.parse
onto the
notes
variable, which we haven't defined,
we'll simply return it to the calling function, as shown here:
var fetchNotes = () => { try{ var notesString = fs.readFileSync('notes-data.json');
return JSON.parse(notesString); } catch (e) {
}
};
So, if I call
fetchNotes
in the
addNote
function, shown as follows, I will get the
notes
array because of the
return
statement in the preceding code.
Now, if there are no notes, maybe there's no file at all; or there is a file, but the
data isn't JSON, we can return an empty array. We'll add a
return
statement inside
of
catch
, as shown in the following code block, because remember,
catch
runs if
anything inside
try
fails:
var fetchNotes = () => { try{ var notesString = fs.readFileSync('notes-data.json');
return JSON.parse(notesString);
} catch (e) { return [];
}
};
Now, this lets us simplify
addNote
even further. We can remove the empty space
and we can take the array that we set on the
notes
variable and remove it and
instead call
fetchNotes
, as shown here:
var addNote = (title, body) => { var notes =
fetchNotes(); var note = { title, body };
With this in place, we now have the exact same functionality we had before,
but we have a reusable function,
fetchNotes
, which we can use in the
addNote
function
to handle the other commands that our app will support.
Instead of copying code and having it in multiple places in your file, we've broken
it into one place. If we ever want to change how this functionality works, whether
we want to change the filename or some of the logic such as the
try
-
catch
block, we
can change it once instead of having to change it in every function we have.
Working with saveNotes
Now, the same thing will go for
saveNotes
just as in the case of the
fetchNotes
function.
The
saveNotes
function will take the
notes
variable and it will say this using
fs.writeFileSync
.
I will cut out the line in
addNote
that does this (that is, fs.writeFileSync('notes-data.json',
JSON.stringfy(notes));) and paste it in the saveNotes function, as shown here:
var saveNotes = (notes) => { fs.writeFileSync('notes-data.json', JSON.stringify(notes)); };
Now,
saveNotes
doesn't need to return anything. In this case, we'll copy the line in
saveNotes and then call
saveNotes
in the
if
statement of the
addNote
function, as shown in
the following code:
if (duplicateNotes.length === 0) {
notes.push(note); saveNotes(); }
This might seem like overkill, we've essentially taken one line and replaced it
with a different line, but it is a good idea to start getting in the habit of creating
reusable functions.
Now, calling
saveNotes
with no data is not going to work, we want to pass in the notes
variable, which is our
notes
array defined earlier in the
saveNotes
function:
if (duplicateNotes.length === 0) {
notes.push(note); saveNotes(notes);
}
With this in place, the
addNote
function should now work as it did before we did
any of our refactoring.
Testing the functionality
The next step in the process will be to test this out by creating a new note. We
already have two notes, with a title of
secret
and a title of
secret2
in
notes-data.json
, let's
make a third one using the
node app.js
command in Terminal. We'll use the add
command and pass in a title of
to buy
and a body of
food
, as shown here: node app.js add -
-title="to buy" --body="food"
This should create a new note, and if I run the command, you can see we don't
have any obvious errors:
Inside of our
notes-data.json
file, if I scroll to the right, we have our brand new note as
a title of
to buy
and a body of
food
:
So, everything is working as expected even though we've refactored the code.
Now, the next thing I want to do inside
addNote
is take a moment to return the note
that's being added, and that will happen right after
saveNotes
comes back. So we'll
return
note
:
if (duplicateNotes.length === 0) {
notes.push(note); saveNotes(notes); return
note; }
This
note
object will get returned to whoever called the function, and in this case,
it will get returned to
app.js
, where we called it in the
if else
block of the
add
command
in the
app.js
file. We can make a variable to store this result and we can call it
note
:
if (command === 'add')
var note = notes.addNote(argv.title, argv.body);
If
note
exists, then we know that the note was created. This means that we can go
ahead and print a message, like
Note created
, and we can print the
note
title and the note
body. Now, if
note
does not exist, if it's undefined, this means that there was a
duplicate and that title already exists. If that's the case, I want you to print an error
message such as
Note title already in use
.
There's a ton of different ways you could do this. The goal, though,
is to print two different messages depending on whether or not a
note was returned.
Now, inside
addNote
, if the
duplicateNotes
if
statement never runs, we don't have an
explicit call to return. But as you know, in JavaScript, if you don't call
return
, then
undefined automatically is returned. This means that if duplicateNotes.length is not equal to
zero, undefined will be returned and we can use that as the condition for our
statement.
The first thing I'll do here is to create an
if
statement, right next to the
note
variable
we defined in
app.js
:
if (command === 'add') { var note = notes.addNote(argv.title,
argv.body); if (note) {
}
This will be an object if things went well, and it will be undefined if things went
poorly. This code in here is only ever going to run if it's an object. The
Undefined
result will fail the condition inside of JavaScript.
Now, if the
note
was created successfully, what we'll do is to print a little message
to the screen, using the following
console.log
statement:
if (note) { console.log('Note created');
}
If things went poorly, inside the
else
clause, we can call
console.log
, and we can print
something like
Note title taken
, as shown here:
if (note) { console.log('Note created');
} else {
console.log('Note title taken');
}
Now, the other thing that we want to do if things went well is print the
notes
content.
I'll do this by first using
console.log
to print a couple of hyphens. This will create a
little space above my note. Then I can use
console.log
twice: the first time we'll print
the title, I'll add
Title:
as a string to show you what exactly you're seeing, then I can
concatenate the title, which we have access to in
note.title
, as shown in this code:
if (note) { console.log('Note created'); console.log('--
'); console.log('Title: ' + note.title);
Now, the preceding syntax uses an ES5 syntax; we can swap this out with an ES6
syntax using what we've already talked about: template strings. We'll add Title, a
colon, and then we can use our dollar sign with our curly braces to inject the
note.title
variable, as shown here: console.log(`Title: ${note.title}`);
Similarly, I'll add
note.body
after this to print out the body of the note. With this in
place, the code should look like:
if (command === 'add') { var note = note.addNote(argv.title,
argv.body); if (note) { console.log('Note created'); console.log('--
'); console.log(`Title: ${note.title}`); console.log(`Body:
${note.body}`);
} else {
console.log('Note title taken');
}
Now, we should be able to run our app and see both of the title and body notes
printed. In Terminal, I'll rerun the previous command. This will try to create a
note with to buy, which already exists, so we should get an error message, and
right here you can see Note title taken:
Now, we can rerun the command, changing the title to something else, such as
to
buy from store. This is a unique
note
title so the note should get created without any
problems:
node app.js add --title="to buy from store" --body="food"
As shown in the preceding output, you can see that we get just that: we have our
Note created message, our little spacer, and our title along with the body.
The
addNote
command is now complete. We have an output when the command
actually finishes, and we have all the code that runs behind the scenes to add the
note to the data that gets stored in our file.
Summary
In this chapter, you learned that parsing in
process.argv
can be a real pain. We would
have to write a lot of manual code to parse out those hyphens, the equal signs,
and the optional quotes. However, yargs can do all of that for us and it puts it on
a really simple object we can access. You also learned how to work with JSON
inside Node.js.
Next, we filled out the
addNote
function. We're able to add notes using the command
line, and we're able to save those notes into a JSON file. Finally, we pulled out a
lot of the code from
addNote
into separate functions,
fetchNotes
and saveNotes, which are
now separate, and they're able to be reused throughout the code. When we start
filling out the other methods, we can simply call
fetchNotes
and
saveNotes
instead of
having to copy the contents over and over again to every new method.
In the next chapter, we'll continue our journey on node fundamentals. We'll
explore some more concepts related to node, such as debugging; we'll work on
the
read
and
remove
notes commands. Apart from this, we'll also learn about the
advanced features of yargs and the arrow function.
Node Fundamentals – Part 3
We start adding support for all the other commands inside of the notes
application. We'll take a look at how we can create our
read
command. The
read
command will be responsible for fetching the body of an individual note. It will
fetch all the notes and print them to the screen. Now, aside from all of that, we'll
be looking at debugging broken apps, and we'll look at some new ES6 features.
You'll learn how to use the built-in Node
debugger
.
Then, you will learn a little bit more about how we can configure yargs for the
command-line interface applications. We'll learn how to set up the commands,
their descriptions, and the arguments. We'll be able to set various properties on
the arguments, for example, whether or not they're required, and others.
Removing a note
In this section, you will write the code for removing a note when someone uses
that
remove
command, and they pass in the title of the note they want to remove. In
the previous chapter, we already created some utility functions that help us with
fetching and saving notes, so the code should actually be pretty simple.
Using the removeNote function
The first step in the process is to fill out the
removeNote
function, which we defined
in the previous chapters, and this will be your challenge. Let's remove
console.log
from the
removeNote
function in the
notes.js
file. You only need to write three lines of
code to get this done.
Now, the first line will fetch the notes, then the job will be to filter out the notes,
removing the one with title of argument. That means we want to go through all
of the notes in the notes array, and if any of them have a title that matches the
title we want to remove, we want to get rid of them. And this can be done using
the
notes.filter
function we used earlier. All we have to do is switch the equality
statement in the
duplicateNotes
function from equals to not equals, and this code will
do just that.
It will go through the notes array. Every time it finds a note that doesn't match the
title it will keep it, which is what we want, and if it does find the title it will return
false
and remove it from the array. And then we will add the third line, which is to
save the new notes array:
var removeNote = (title) => {
// fetch notes
// filter notes, removing the one with title of argument
// save new notes array
};
The preceding code lines are the only three lines you need to fill out. Don't worry
about returning anything from
removeNote
or filling out anything inside of app.js.
The first thing we will do for the
fetchNotes
line is to create a variable called
notes
,
just like we did in
addNote
in the previous chapter, and we'll set it equal to the return
result from
fetchNotes
:
var removeNote = (title) => { var notes =
fetchNotes();
// filter notes, removing the one with title of argument
// save new notes array
};
At this point our notes variable stores an array of all of the notes. The next thing
we need to do is filter our notes.
If there is a note that has this title, we want to remove it. This will be done by
creating a new variable, and I'll call this one
filteredNotes
. Here we'll set filteredNotes equal
to the result that will come back from
notes.filter
, which we already used up
previously:
var removeNote = (title) => { var notes =
fetchNotes();
// filter notes, removing the one with title of argument var filteredNotes =
notes.filter();
// save new notes array
};
We know that
notes.filter
takes a function as its one and only argument, and that
function gets called with the individual item in the array. In this case it would be
a
note
. And we can do this all on one line using the ES6 arrow syntax.
If we have only one statement, we don't need to open and close curly
braces.
That means right here we can return
true
if
note.title
does not equal the title that's
passed into the function:
var removeNote = (title) => { var notes = fetchNotes(); var filteredNotes = notes.filter((note) =>
note.title !== title);
// save new notes array
};
This will populate
filteredNotes
with all of the notes whose titles do not match the one
passed in. If the title does match the title passed in, it will not be added to filteredNotes
because of our filter function.
The last thing to do is to call
saveNotes
. Right here, we'll call
saveNotes
passing in the
new notes array which we have under the
filteredNotes
variable:
var removeNote = (title) => { var notes = fetchNotes(); var filteredNotes = notes.filter((note) =>
note.title !== title); saveNotes(filteredNotes);
// save new notes array
};
If we were to pass in notes, it wouldn't work as expected; we're filtering the
notes out but we're not actually saving those notes, so it will not get removed
from the JSON. We need to pass
filteredNotes
as shown in the preceding code. And
we can test these by saving the file and trying to remove one of our notes.
I'll try to remove
secret2
from the
notes-data.json
file. That means all we need to do is
run the command, which we called
remove
, that is specified over in
app.js
, (refer to
the following code image, and then it will call our function).
I'll run Node with
app.js
, and we'll pass in the
remove
command. The only argument
we need to provide for remove is the title; there's no need to provide the body. I'll
set this equal to
secret2
:
Although we do have the command remove printing, there is no message saying
whether or not a note was removed, but we'll add that later in the section.
For now, we can check the data. And right here you can see
secret2
is nowhere in
sight:
As shown in the screenshot, if
I hit
enter
you can see we don't get any
output.
This means our remove method is indeed working as expected. It removed the
note whose title matched and it kept all the notes whose title was not equal to
secret2, exactly what we wanted.
Printing a message of removing
notes
Now, the next thing we'll do is print a message depending on whether or not a
note was actually removed. That means
app.js
, which calls the
removeNote
function,
will need to know whether or not a note was removed. And how do we figure that
out? How can we possibly return that given the information we have in notes.js
removeNotes function?
Well, we can, because we have two really important pieces of information. We
have the length of the original notes array and we have the length of the new notes
array. If they're equal then we can assume that no note was removed. If they are
not equal, we'll assume that a note was removed. And that is exactly what we'll
do.
If the
removeNote
function returns
true
, that means a note was removed; if it returns
false, that means a note was not removed. In the
removeNotes
function we can add
return, as shown in the following code. We'll check if
notes.length
does not equal
filteredNotes.length:
var removeNote = (title) => { var notes = fetchNotes(); var filteredNotes = notes.filter((note) =>
note.title !== title); saveNotes(filteredNotes);
return notes.length !== filteredNotes.length; };
If they're not equal it will return
true
, which is what we want because a note
was removed. If they're equal it will return
false
, which is great.
Now, inside of
app.js
we can add a few lines in the
removeNote
,
else if
block to make the
output for this command a little nicer. The first thing to do is to store that Boolean.
I'll make a variable called
noteRemoved
and we'll set that equal to the return, result as
shown in the following code, which will either be
true
or
false
:
} else if (command == 'remove') {
var noteRemoved = notes.removeNote(argv.title); }
On the next line, we can create our message, and I'll do this all on one line
using the ternary operator. Now, the ternary operator lets you specify a condition.
In our case, we'll use a var message and it will be set equal to the condition
noteRemoved, which will be
true
if a note was removed and
false
if it wasn't.
Now, the ternary operator can be a little confusing, but it's really
useful inside JavaScript and Node.js. The format for the ternary
operator is first we add the condition, question mark, the truthy
expression to run, colon, and then the falsy expression to run.
After the condition, we'll put a space with a question mark and a space; this is the
statement that will run if it's true. If the
noteRemoved
condition passes, what we want
to do is set message equal to
Note was removed
: var message = noteRemoved ? 'Note was removed' :
Now, if
noteRemoved
is
false
, we can specify that condition right after the colon in the
previous statement. Here, if there is no note removed we'll use the text
Note
not found: var message = noteRemoved ? 'Note was removed' : 'Note not found';
Now with this in place, we can test out our message. The last thing to do is print
the message to the screen using
console.log
passing in message:
var noteRemoved = notes.removeNote(argv.title); var message = noteRemoved ? 'Note was
removed' : 'Note not found'; console.log(message);
This lets us avoid
if
statements that make our
else-if
clause to remove unnecessarily
complex.
Back inside of Atom we can rerun the last command, and in this case no note will
get removed because we already deleted it. And when I run it, you can see that
Note not found
prints to the screen:
Now I'll remove a note that does exist; in
notes-data.json
I have a note with a title of
secret as shown here:
Let's rerun the command removing the
2
from the title in Terminal. When I run
this command, you can see
Note was removed
prints to the screen:
That is it for this section; we now have our
remove
command in place.
Reading note
In this section, you will be responsible for filling out the rest of the
read
command.
Now, the
read
command does have an else-if block to find in
app.js
where we call
getNote
:
} else if (command === 'read') { notes.getNote(argv.title);
getNote is defined over inside
notes.js
, even though currently it just prints out some
dummy text:
var getNote = (title) => {
console.log('Getting note', title);
};
What you'll need to do in this section is wire up both of these functions.
First up, you will need to do something with the return value from
getNote
. Our getNote
function will return the note object if it finds it. If it doesn't, it will return
undefined just like we do for
addNote
discussed in the section Adding and saving
note, in the previous chapter.
After you store that value, you'll do some printing using
console.log
, similar to what
we have here:
if (command === 'add') { var note = notes.addNote(argv.title,
argv.body); if (note) { console.log('Note created'); console.log('--');
console.log(`Title: ${note.title}`); console.log(`Body: ${note.body}`);
} else {
console.log('Note title taken');
}
Obviously, Note created will be something like Note read and Note title taken will be
something like
Note not found
, but the general flow is going to be exactly the same.
Now, once you have that wired up inside of
app.js
, you can move on to notes.js, filling
out the function.
Now, the function inside of
notes.js
isn't going to be that complex. All you need to
do is fetch the notes, like we've done in previous methods, then you're going to
use
notes.filter
, which we explored to only return notes whose title matches the title
passed in as the argument. Now, in our case this is either going to be zero notes,
which means the note is not found, or it's going to be one note, which means
we've found the note that the person wants to return.
Next, we do need to return that note. It's important to remember the return value
from
notes.filter
is always going to be an array, even if that array only has one item.
What you're going to need to do is return the first item in the array. If that item
doesn't exist that's fine, it'll return undefined, as we want. If it does exist, great,
that means we found the note. This method only requires three lines of code, one
for fetching, one for filtering, and the return statement. Now, once you have all
that done we'll test it out.
Using the getNote function
Let's work on this method. Now, the first thing I'll do is fill out, inside of
app.js
, a
variable called note which is going to store the return value from
getNote
:
} else if (command === 'read') { var note =
notes.getNote(argv.title);
Now, this could be an individual note object or it could be undefined. In the next
line, I can use an
if
statement to print the message if it exists, or if it does not exist.
I'll use
if
note, and I am going to attach an
else
clause:
} else if (command === 'read') { var note =
notes.getNote(argv.title); if (note) {
} else {
}
This
else
clause will be responsible for printing an error if the note is not found.
Let's get started with that first since it's pretty simple,
console.log
,
Note not found
, as shown
here:
if (note) { }
else {
console.log('Note not found');
}
Now that we have our
else
clause filled out we can fill out the if statement. For
this, I'll print a little message,
console.log ('Note found')
will get the job done. Then we can
move on to printing the actual note details, and we already have that code in place.
We are going to add the hyphenated spacer, then we have our note title and our
note body as shown here:
if (note) { console.log('Note found'); console.log('--');
console.log(`Title: ${note.title}`); console.log(`Body:
${note.body}`);
} else {
console.log('Note not found'); }
Now that we're done with the inside of
app.js
, we can move into the
notes.js
file
and fill out the
getNote
method because currently it doesn't do anything with the
title that gets passed in.
Inside notes, what you needed to do was fill out those three lines. The first one is
going to be responsible for fetching the notes. We already have did that before
with the
fetchNotes
function in the previous section:
var getNote = (title) => { var notes =
fetchNotes();
};
Now that we have our notes in place we, can call
notes.filter
, returning all of the
notes. I'll make a variable called
filteredNotes
, setting it equal to
notes.filter
. Now, we
know that the filter method takes a function, I'll define an arrow function (
=>
) just
like this:
var filteredNotes = notes.filter(() => {
});
Inside the arrow function (
=>
), we'll get the individual note passed in, and we'll
return
true
when the note title, the title of the note we found in our JSON file,
equals, using triple equals, title:
var filteredNotes = notes.filter(() => { return note.title === title;
});
};
This will return
true
when the note title matches and false if it doesn't.
Alternatively, we can use arrow functions, and we only have one line, as shown
following, where we return something; we can cut out our condition, remove the
curly braces, and simply paste that condition right here: var filteredNotes = notes.filter((note) =>
note.title === title);
This has the exact same functionality, only it's a lot shorter and easier to look at.
Now that we have all of the data, all we need to do is return something, and we'll
return the first item in the
filteredNotes
array. Next, we'll grab the first item, which is
the index of zero, and then we just need to return it using the
return
keyword:
var getNote = (title) => { var notes = fetchNotes(); var filteredNotes = notes.filter((note) =>
note.title === title); return filteredNotes[0];
};
Now, there is a chance that
filteredNotes
, the first item, doesn't exist, and that's fine,
it's going to return undefined, in which case our else clause will run, printing
Note
not found
. If there is a note, great, that's the note we want to print, and over in
app.js
we do just that.
Running the getNote function
Now that we have this in place we can test out this brand new functionality inside
of Terminal by running our app using
node app.js
. I'll use the
read
command, and I'll
pass in a title equal to some string that I know does not exist inside of a title in
the
notes-data.json
file: node app.js read --title="something here"
When I run the command, we get
Note not found
, as shown here, and this is exactly
what we want:
Now, if I do try to fetch a note where the title does exist, I would expect that note
to come back.
In the data file I have a note with a title of
to buy
; let's try to fetch that one. I'll use
the up arrow key to populate the previous command and replace the title with
to
space
, buy, and hit enter:
As shown in the previous code, you can see
Note found
prints to the screen, which is
fantastic. Following
Note found
we have our spacers and following that we have the
title, which is
to buy
, and the body, which is
food
, exactly as it appears inside of the
data file. With this in place, we are done with the
read
command.
The DRY principle
Now, there is one more thing I want to tackle before we wrap up this section.
Inside
app.js
we now have the same code in two places. We have the space or title
body in the
add
command as well as in the
read
command:
if (command === 'add') { var note = notes.addNote(argv.title,
argv.body); if (note) { console.log('Note created'); console.log('--');
console.log(`Title: ${note.title}`); console.log(`Body: ${note.body}`);
} else {
console.log('Note title taken');
}
} else if (command === 'list') { notes.getAll();
} else if (command === 'read') { var note =
notes.getNote(argv.title); if (note) { console.log('Note
found'); console.log('--'); console.log(`Title:
${note.title}`); console.log(`Body: ${note.body}`);
} else {
console.log('Note not found');
}
When you find yourself copying and pasting code, it's probably best to break
that out into a function that both locations call. This is the DRY principle,
which stands for Don't Repeat Yourself.
Using the logNote function
In our case, we are repeating ourselves. It would be best to break this out into a
function that we can call from both places. In order to do this, all we're going to
do is make a function in
notes.js
called
logNote
.
Now, in
notes.js
, down following the
removeNote
function, we can make that brand new
function a variable called
logNote
. This is going to be a function that takes one
argument. This argument will be the note object because we want to print both
the title and the body. As shown here, we'll expect the note to get passed in:
var logNote = (note) => {
};
Now, filling out the
logNote
function is going to be really simple, especially when
you're solving a DRY issue, because you can simply take the code that's repeated,
cut it out, and paste it right inside the
logNote
function. In this case the variable
names line up already, so there is no need to change anything:
var logNote = (note) => { console.log('--');
console.log(`Title: ${note.title}`); console.log(`Body:
${note.body}`);
};
Now that we have the
logNote
function in place, we can change things over in app.js.
In
app.js
, where we have removed the
console.log
statements we can call notes.logNote,
passing in the note object just like this:
else if (command === 'read') { var note =
notes.getNote(argv.title); if (note) {
console.log('Note found'); notes.logNote(note);
} else {
console.log('Note not found');
}
And we can do the same thing in case of the
add
command
if
block. I can remove
these three
console.log
statements and call
notes.logNote
, passing in note:
if (command === 'add') { var note = notes.addNote(argv.title,
argv.body); if (note) { console.log('Note created');
notes.logNote(note);
} else {
console.log('Note title taken');
}
And now that we have this in place, we can rerun our program and hopefully what
we see is the exact same functionality.
The last thing to do before we rerun the program is export the
logNote
function in
exports module in
notes.js
file.
LogNote
is going to get exported and we're using the ES6
syntax to do that:
module.exports = {
addNote, getAll,
getNote, removeNote,
logNote };
With this in place, I can now rerun the previous command from Terminal
using up and hit enter: node app.js read --title="to buy"
As shown, we get
Note found
printing to the screen, with the title and the body just
like we had before. I'm also going to test out the
add
command to make sure that
one's working,
node app.js add
; we will use a title of things to do and a body of go to
post office: node app.js add --title="things to do" --body="go to post office"
Now, when I hit enter, we would expect the same log to print as it did before for
the
add
command, and that's exactly what we get:
Note created prints, we get our spacer, and then we get our title and our body.
In the next section, we're going to cover one of the most important topics in the
book; which is debugging. Knowing how to properly debug programs is going to
save you literally hundreds of hours over your Node.js career. Debugging can be
really painful if you don't have the right tools, but once you know how it's done,
it really isn't that bad and it can save you a ton of time.
Debugging
In this section, we're going to use the built-in
debugger
, which can look a little
complex because it's run inside of the command line. That means that you have
to use the command-line interface, which is not always the most pleasant thing to
look at. In the next section, though, we are going to be installing a third-party tool
that uses Chrome DevTools in order to debug your Node app. That one looks
great because the Chrome DevTools are fantastic.
Executing a program in debug
mode
Before going ahead, we will learn that we do need to create a place to play around
with debugging and that's going to happen in a playground file, since the code
we're going to write is not going to be important to the
notes
app itself. Inside the
notes app I'll make a new file called
debugging.js
:
In
debugging.js
we're going to start off with a basic example. We're going to make an
object called
person
, and on that object for the moment, we're going to set one
property name. Set it equal to your name, I'll set mine equal to the string
Andrew
as
shown:
var person = { name:
'Andrew' };
Next up we're going to set another property, but in the next line,
person.age
. I'll set
mine equal to my age,
25
:
var person = { name:
'Andrew'
}; person.age = 25;
Then we're going to add another statement that changes the name,
person.name
equals
something like
Mike
:
var person = { name:
'Andrew'
}; person.age = 25;
person.name = 'Mike';
Finally, we're going to
console.log
the
person
object, the code is going to look like this:
var person = { name:
'Andrew'
}; person.age = 25;
person.name = 'Mike';
console.log(person);
Now, we actually already have a form of debugging in this example, we have a
console.log statement.
As you're going through the Node application development process, you may or
may not have used
console.log
to debug your app. Maybe something's not working as
expected and you want to figure out exactly what that variable has stored inside
of it. For example, if you have a function that solves a math problem, maybe at
one part in the function the equation is wrong and you're getting a different result.
Using
console.log
can be a pretty great way to do that, but it's super limited. We can
view that by running it from Terminal, I'll run the following command for this:
node playground/debugging.js
When I run the file, I do get my object printed out to the screen, which is great,
but, as you know, if you want to debug something besides the
person
object you
have to add another
console.log
statement in order to do that.
Imagine you have something like our
app.js
file, you want to see what command
equals, then you want to see what
argv
equals, it could take a lot of time to add and
remove those
console.log
statements. There is a better way to debug. This is using the
Node
debugger
. Now, before we make any changes to the project, we'll take a look
at how the
debugger
works inside of Terminal, and as I warned you in the beginning
of the section, the built-in Node
debugger
, while it is effective, is a little ugly and
hard to use.
For now, though, we are going to run the app much the same way, only this time
we're going to type
node inspect
. Node debug is going to run our app completely
differently from the regular Node command. We're running the same file in the
playground folder, it's called
debugging.js
: node inspect playground/debugging.js
When you hit enter, you should see something like this:
In the output, we can ignore the first two lines. This essentially means that the
debugger was set up correctly and it's able to listen to the app running in the
background.
Next, we have our very first line break in playground debugging on line one, and
right following to it you can see line one with a little caret (
>
) next to it. When
you first run your app in debug mode, it pauses before it executes the first
statement. When we're paused on a line like line one, that means the line has not
executed, so at this point in time we don't even have the
person
variable in place.
Now, as you can see in the preceding code, we haven't returned to the command
line, Node is still waiting for input, and there are a few different commands we
can run. For example, we can run
n
, which is short for next. You can type
n
, hit
enter, and this moves on to the next statement.
The next statement we have, the statement on line one, was executed, so the person
variable does exist. Then I can use
n
again to go to the next statement where we
declare the
person.name
property, updating it from
Andrew
to
Mike
:
Notice, at this point, age does exist because that line has already been executed.
Now, the
n
command goes statement by statement through your entire program.
If you realize that you don't want to do that through the whole program, which
could take a lot of time, you can use
c
. The
c
command is short for Continue, and
that continues to the very end of the program. In the following code, you can see
our
console.log
statement runs the name
Mike
and the age
25
:
This is that's a quick example of how to use the
debug
keyword.
Now, we actually didn't do any debugging, we just ran through the program since
it is a little foreign in terms of writing these commands, such as next and continue,
I decided to do a dry run once with no debugging. You can use control + C to
quit the
debugger
and get returned back to Terminal.
I'll use
clear
to clear all the output. Now that we have a basic idea about how we
can execute the program in
debug
mode, let's take a look at how we can actually do
some debugging.
Working with debugging
I'll rerun the program using the up arrow key twice to return to the Node
debug
command. Then, I'll run the program, and I'll hit next twice,
n
and
n
:
At this point in time, we are on line seven, that is where the line break currently
is. From here we can do some debugging using a command called
repl
, which
stands for Read Evaluate Print Loop. The
repl
command, in our case, brings you
to an entirely separate area of the
debugger
. When you hit it you're essentially in a
Node console:
You can run any Node commands, for example, I can use
console.log
to print
something like
test
, and test prints up right there.
I can make a variable
a
that is equal to
1
plus
3
, then I can reference
a
and I can see
it's equal to
4
as shown:
More importantly, we have access to the current program as it sits, meaning as it
was before line seven was executed. We can use this to print out
person
, and as
shown in the following code, you can see the person's name is
Andrew
because line
seven hasn't executed and the age is
25
, exactly as it appears in the program:
This is where debugging gets really useful. Being able to look at the program
paused at a certain point in time is going to make it really easy to spot errors. I
could do anything I want, I could print out the
person
name property, and that prints
Andrew
to the screen, as shown here:
Now, once again, we still have this problem. I have to hit next through the
program. When you have a really long program, there could literally be hundreds
or thousands of statements that need to run before you get to the point you care
about. Obviously that is not ideal, so we're going to look at a better way.
Let's quit
repl
using control + C; now we're back at the
debugger
.
From here we are going to make a quick change to our application in
debugging.js
.
Let's say we want to pause line seven between the person age property update and
the person name property update. In order to pause, what we're going to do is run
the statement
debugger
:
var person = { name:
'Andrew'
}; person.age = 25;
debugger;
person.name =
'Mike';
console.log(person);
When you have a
debugger
statement exactly like previous, it tells the Node debugger
to stop here, which means instead of using
n
(next) to go statement by statement,
you can use
c
(continue), which is going to continue until either the program exits
or it sees one of the
debugger
keywords.
Now, over in Terminal, we're going to rerun the program exactly like we did
before. This time around, instead of hitting
n
twice, we're going to use
c
to
continue:
Now, when we first used
c
, it went to the end of the program, printing out our
object. This time around it's going to continue until it finds that
debugger
keyword.
Now, we can use
repl
, access anything we like, for example,
person.age
, shown in this
code:
Once we're done debugging, we can quit and continue through the program.
Again, we can use control + C to quit
repl
and the
debugger
.
All real debugging pretty much happens with the
debugger
keyword. You put it
wherever you want on your program, you run the program in debug mode,
eventually it gets to the
debugger
keyword and you do something. For example you
explore some variable values, you run some functions, or you play around with a
code to find the error. No one really uses
n
to print through the program, finding
the line that causes the problem. That takes way too much time and it's just not
realistic.
Using debugger inside the notes
application
Now that you know a little bit about the
debugger
, I want you to use it inside our
notes application. What we will do inside
notes.js
is add the
debugger
statement in logNote
function as the first line of the function. Then I will run the program in debug
mode, passing in some arguments that will cause
logNote
to run; for example,
reading a note, after the note gets fetched, it's going to call
logNote
.
Now, once we have the
debugger
keyword in the
logNote
function and run it in debug
mode with those arguments, the program should stop at this point. Once the
program starts in debug mode, we'll use
c
to continue, and it'll pause. Next, we'll
print out the note object and make sure it looks okay. Then, we can quit
repl
and
quit the
debugger
.
Now, first we are adding the
debugger
statement right here:
var logNote = (note) => { debugger; console.log('--');
console.log(`Title: ${note.title}`); console.log(`Body:
${note.body}`);
};
We can save the file, and now we can move into Terminal; there's no need to do
anything else inside our app.
Inside Terminal we're going to run our
app.js
file,
node debug app.js
, because we want to
run the program in debug mode. Then we can pass in our arguments, let's say the
read
command, and I'll pass in a title,
"to buy"
as shown here: node debug app.js read --title="to
buy"
In this case I have a note with the title
"to buy"
, as shown here:
Now, when I run the preceding command, it's going to pause before that first
statement runs, this is expected:
I can now use
c
to continue through the program. It's going to run as many
statements as it takes for either the program to end or for the
debugger
keyword to
be found, and as shown in the following code, you can see the
debugger
was found
and our program has stopped on line
49
of
notes.js
:
This is exactly what we wanted to do. Now, from here, I'll go into
repl
and print
out note argument, and as shown in the following code, you can see we have the
note with the title of
to buy
and the body
food
:
Now, if there was an error in this statement, maybe the wrong thing was printing
to the screen, this would give us a pretty good idea as to why. Whatever gets
passed into the
note
is clearly being used inside of the
console.log
statements, so if
there was an issue with what's printing, it's most likely an issue with what gets
passed into the
logNote
function.
Now that we've printed the
note
variable, we can shut down
repl
, and we can use
control + C or
quit
to quit the
debugger
.
Now we're back at the regular Terminal and we have successfully completed the
debugging inside the Node application. In the next section, we're going to look at
a different way to do the same thing, a way with a much nicer graphic user
interface that I find a lot easier to navigate and use.
Listing notes
Now that we've made some awesome progress on debugging, let's go back to the
commands for our app, because there is only one more to fill out (we have covered
the
add
,
read
, and
remove
commands in the
Chapter 3
, Node Fundamentals Part 2, and
this chapter, respectively). It's the
list
command, and it's going to be really easy,
there is nothing complex going on in the case of the
list
command.
Using the getAll function
In order to get started, all we need to do is fill out the list notes function, which
in this case we called
getAll
. The
getAll
function is responsible for returning every
single note. That means it's going to return an array of objects, an array of all of
our notes.
All we have to do that is to return
fetchNotes
, as shown here:
var getAll = () => { return
fetchNotes();
}
There's no need to filter, there's no need to manipulate the data, we just need to
pass the data from
fetchNotes
back through
getAll
. Now that we have this in place, we
can fill out the functionality over inside of
app.js
.
We have to create a variable where we can store the notes, I was going to call it
notes, but I probably shouldn't because we already have a notes variable declared.
I'll create another variable, called
allNotes
, setting it equal to the return value from
getAll
, which we know because we just filled out returns all the notes:
else if (command === 'list') { var allNotes =
notes.getAll();
}
Now I can use
console.log
to print a little message and I'll use template strings so I
can inject the actual number of notes that are going to be printed.
Inside the template strings, I'll add
Printing
, then the number of notes using the
$
(dollar) sign and the curly braces,
allNotes.length
: that's the length of the array followed
by notes with the
s
in parenthesis to handle both singular and plural cases, as
shown in the following code block:
else if (command === 'list') { var allNotes = notes.getAll();
console.log(`Printing ${allNotes.length} note(s).`); }
So, if there were six notes, it would say printing six notes.
Now that we have this in place, we have to go about the process of actually
printing each note, which means we need to call
logNote
once for every item in the
allNotes
array. To do, this we'll use
forEach
, which is an array method similar to filter.
Filter lets you manipulate the array by returning
true
or
false
to keep items or remove
items;
forEach
simply calls a callback function once for each item in the array. In
this case we can use it using
allNotes.forEach
, passing in a callback function. Now, that
callback function will be an arrow function (
=>
) in our case, and it will get called
with the
note
variable just like filter would have. And all we'll call is
notes.logNote
,
passing in the
note
argument, which is right here:
else if (command === 'list') { var allNotes = notes.getAll();
console.log(`Printing ${allNotes.length} note(s).`); allNotes.forEach((note) => {
notes.logNote(note);
});
}
And now that we have this in place, we can actually simplify it by adding the
logNote call, as shown in here:
else if (command === 'list') { var allNotes = notes.getAll();
console.log(`Printing ${allNotes.length} note(s).`); allNotes.forEach((note) =>
notes.logNote(note)); }
This is the exact same functionality, only using the expression syntax. Now that
we have our arrow function (
=>
) in place, we are calling
notes.logNote
once for each
item in the all notes array. Let's save the
app.js
file and test this out over in Terminal.
In order to test out the
list
command, all I'll use is
node app.js
with the command list.
There is no need to pass in any arguments: node app.js list
When I run this, I do get Printing 3 note(s) and then I get my 3 notes to buy, to buy from
store
, and
things to do
, as shown in the following code output, which is fantastic:
With this in place, all of our commands are now working. We can add notes,
remove notes, read an individual note, and list all of the notes stored in our JSON
file.
Moving on to the next section, I want to clean up some of the commands. Inside
app.js and
notes.js
, we have some
console.log
statements that are printing out a few things
we no longer need.
At the very top of app.js, I am going to remove the console.log('Starting app.js') statement,
making the constant
fs
the first line.
I'll also remove the two statements: console.log('Command: ', command) and console.log('Yargs', argv) that
print the command and the
yargs
variable value.
Inside notes.js, I will also remove the console.log('Stating notes.js') statement at the very top of
that file, since it is no longer necessary, putting constant
fs
at the top.
It was definitely useful when we first started exploring different files, but now we
have everything in place, there's no need. If I rerun the
list
command, this time you
can see it looks a lot cleaner:
Printing three notes is the very first line showing up. With this in place, we have done
our commands.
In the next section, we're going to take a slightly more in-depth look at how we
can configure yargs. This is going to let us require certain arguments for our
commands. So if someone tries to add a note without a title, we can warn the user
and prevent the program from executing.
Advanced yargs
Before we get into the advanced discussion of yargs, first, I want to pull up the
yargs docs so that you at least know where the information about yargs is coming
from. You can get it by Googling
npm yargs
. We're going to go to the yargs package
page on npm. This has the documentation for yargs, as shown here:
Now there is no table of contents for the yargs docs, which makes it kind of
difficult to navigate. It starts off with some examples that don't go in any
particular order, and then eventually it gets into a list of all the methods you have
available, and that's what we're looking for.
So I'll use command + F (Ctrl + F) to search the page for methods, and as shown
in the following screenshot, we get the methods header, which is the one we're
looking for:
If you scroll down on the page, we start to see an alphabetical list of all the
methods you have access to inside of yargs. We're specifically looking for .command;
this is the method we can use to configure all four of our commands: the add, read,
remove and list notes:
We're going to specify which options they require, if any, and we can also set up
things like descriptions and help functionality.
Using chaining syntax on yargs
Now in order to get started, we need to make some changes inside of
app.js
. We're
going to start with the
add
command (for more information, please refer to the
Adding and saving notes section in the previous chapter).
We want to add a few helpful pieces of information in
argv
function inside
app.js
,
that will:
Let yargs verify the
add
command is ran appropriately, and
Let the user know how the
add
command is meant to be executed
Now we are going to be chaining property calls, which means right before I access
.argv
I want to call
.command
, and then I'll call
.argv
on the return value from command
as shown here:
const argv = yargs
.command()
.argv;
Now this chaining syntax probably looks familiar if you've used jQuery, a lot of
different libraries are supported. Once we call
.command
on
yargs
, we're going to pass
in three arguments.
The first one is the command name, exactly how the user is going to type it in
Terminal, in our case it's going to be
add
:
const argv = yargs
.command('add')
.argv;
Then we're going to pass another string in, and this is going to be a description of
what the command does. It is going to be some sort of English readable
description that a user can read to figure out weather that's the command that they
want to run:
const argv = yargs
.command('add', 'Add a new note')
.argv;
The next one is going to be an object. This is going to be the options object that
lets us specify what arguments this command requires.
Calling the .help command
Now before we get into the options object, let's add one more call right after
command. We're going to call
.help
, which is a method, so we're going to call it as
a function, and we don't need to pass in any arguments:
const argv = yargs
.command('add', 'Add a new note', {
})
.help()
.argv;
When we add on this help call, it sets up
yargs
to return some really useful
information when someone runs the program. For example, I can run the
node
app.js
command with the
help
flag. The
help
flag is added because we called that help
method, and when I run the program, you can see all of the options we have
available:
a
help
option for the current command,
help
. And the same thing holds true if we run
the node app.js add command with help as shown here: node app.js add --help
In this output, we can view all of the options and arguments for
add
command,
which in this case happens to be none because we haven't set those up:
As shown in the preceding output, we have one
command,
,
Adding the options object
Let's add options and arguments back inside Atom. In order to add properties,
we're going to update the options object, where the key is the property name,
whether it's title or body, and the value is another object that lets us specify how
that property should work, as shown here:
const argv = yargs
.command('add', 'Add a new note', { title: {
}
})
.help()
.argv;
Adding the title
In the case of title, we would add the title on the left-hand side, and we would put
our options object on the right-hand side. Inside the title, we're going to configure
three properties describe, demand, and alias:
The
describe
property will be set equal to a string, and this is going to describe what
is supposed to be passed in for the title. In this case, we can just use
Title of
note:
const argv = yargs
.command('add', 'Add a new note', { title: {
describe: 'Title of note'
}
})
.help()
.argv;
Next we configure
demand
. It is going to tell yarg whether or not this argument is
required.
demand
is
false
by default, we'll set it to
true
:
const argv = yargs
.command('add', 'Add a new note', { title: {
describe: 'Title of note', demand: true
}
})
.help() .argv;
Now if someone tries to run the add command without the title, it's
going to fail, and we can prove this. We can save
app.js
, and in
Terminal, we can rerun our previous command removing the
help
flag, and when I do that, you see we get a warning,
Missing required
argument:
title as shown here:
Notice that in the output the title argument, is
Title of note
, which is the
describe string we used, and it's
required
on the right side, letting you
know that you have to provide a title when you're calling that
add
command.
Along with
describe
and
demand
we are going to provide a third option, this is called
alias
. The
alias
lets you provide a shortcut so you don't have to type
-
title; you can set
the alias equal to a single character like
t
:
const argv = yargs
.command('add', 'Add a new note', { title: {
describe: 'Title of note', demand: true, alias: 't'
}
})
.help()
.argv;
When you have done that, you can now run the command in Terminal using the
new syntax.
Let's run our add command,
node app.js add
, instead of
--title
. We're going to use -
t
, which
is the flag version, and we can set that equal to whatever we like, for example,
flag
title
will be the title, and
--body
will get set equal to
body
, as shown in the following
code. Note that we haven't set up the body argument yet so there is no
alias
: node app.js
add -t="flag title" --body="body"
If I run this command, everything works as expected. The flag title shows up right
where it should, even though we used the alias version which is the letter
t
, as
shown here:
Adding the body
Now that we have our title configured, we can do the exact same thing for the
body. We'll specify our options object and provide those three arguments: describe,
demand, and alias for body:
const argv = yargs
.command('add', 'Add a new note', { title: {
describe: 'Title of note', demand: true, alias: 't'
}, body: {
}
})
.help()
.argv;
The first one is
describe
and that one's pretty easy.
describe
is going to get set equal to
a string, and in this case
Body of note
will get the job done:
const argv = yargs
.command('add', 'Add a new note', { title: {
describe: 'Title of note', demand: true, alias: 't'
}, body: { describe: 'Body of note'
}
})
.help()
.argv;
The next one will be
demand
, and to add a note we are going to need a
body
. So
we'll set
demand
equal to
true
, just like we do up previous for
title
:
const argv = yargs
.command('add', 'Add a new note', { title: {
describe: 'Title of note', demand: true, alias: 't'
}, body: { describe: 'Body of note' demand:
true
}
})
.help()
.argv;
And last but not least is the
alias
. The
alias
is going to get set equal to a single letter,
I'll use the letter
b
for
body
:
const argv = yargs
.command('add', 'Add a new note', { title: {
describe: 'Title of note', demand: true, alias: 't'
}, body: { describe: 'Body of note' demand:
true, alias: 'b'
}
})
.help()
.argv;
With this in place, we can now save
app.js
and inside Terminal, we can take a
moment to rerun node app.js add with the help flag: node app.js add --help
When we run this command, we should now see the body argument showing up,
and you can even see it shows the flag version, as shown in the following output,
the alias
-b
(
Body of note
), and it is required:
Now I'll run
node app.js add
passing in two arguments
t
. I'll set that equal to
t
, and b
setting it equal to
b
.
of
t
and a body of
b
. With this in place, we've now successfully completed the
setup for the
add
command. We have our
add
command title, a description, and the
When I run the command, everything works as
expected:
As shown in the preceding output screenshot, a new note was created
with a title
block that specifies the arguments for that command. Now we do have three more
commands to
add
support for, so let's get started doing that.
Adding support to the read and
remove commands
On the next line, I'll call
.command
again, passing in the command name. Let's do the
list
command first because this one is really easy, no arguments are required.
Then we'll pass in the description for the
list
command,
List all notes
, as shown here:
.command('list', 'List all notes')
.help()
.argv;
Next up, we'll call command again. This time we'll do the command for
read
. The
read command reads an individual note, so for the description for the
read
command,
we'll use something like
Read a note
:
.command('list', 'List all notes')
.command('read', 'Read a note')
.help()
.argv;
Now the
read
command does require the title argument. That means we are going
to need to provide that options object. I'll take
title
from
add
command, copy it, and
paste it in the
read
command options object:
.command('list', 'List all notes')
.command('read', 'Read a note', { title: {
describe: 'Title of note', demand: true,
alias: 't'
}
})
.help()
.argv;
As you probably just noticed, we have repeated code. The title configuration just
got copied and pasted into multiple places. It would be pretty nice if this was
DRY, if it was in one variable we could reference in both locations, in
add
and read
commands.
Will call command for
remove
, just following where we called the command for read.
Now, the
remove
command will have a description. We'll stick with something
simple like
Remove a note
, and we will be providing an options object:
.command('remove', 'Remove a note', {
})
Now I can add the options object identical to the
read
command. However, in that
options object, I'll set title equal to
titleOptions
, as shown here, to avoid the repetition
of code:
.command('remove', 'Remove a note', { title: titleOptions
})
Adding the titleOption and
bodyOption variables
Now I don't have the
titleOptions
object created yet so the code would currently fail,
but this is the general idea. We want to create the
titleOptions
object once and
reference it in all the locations we use it, for
add
,
read
and
remove
command. I can
take
titleOptions
, and add it for
read
as well as for
add
command, as shown here:
.command('add', 'Add a new note', { title:
titleOptions, body: { describe: 'Body of note',
demand: true, alias: 'b'
}
})
.command('list', 'List all notes')
.command('read', 'Read a note', { title: titleOptions
})
.command('remove', 'Remove a note', { title: titleOptions
})
Now, just previous the constant
argv
, I can create a constant called
titleOptions
,
and I can set it equal to that object that we defined for
add
and
read
command earlier,
which is
describe
,
demand
, and
alias
, as shown here:
const titleOptions = { describe: 'Title of
note', demand: true, alias: 't' };
We now have the
titleOptions
in place, and this will work as expected. We have
the exact same functionality we did before, but we now have the
titleOptions
in a
separate object, which follows the DRY principle we discussed in the Reading
note section.
Now, we could also do the same thing for body. It might seem like overkill since
we're only using it in only one location, but if we're sticking to the pattern of
breaking them out into variables, I'll do it in the case of the body as well. Just
following the
titleOptions
constant, I can create the constant
bodyOptions
, setting it equal
to the options object we defined in the body, for
add
command in the previous
subsection:
const bodyOptions = { describe:
'Body of note', demand: true, alias:
'b' };
With this in place, we are now done. We have
add
,
read
, and
remove
, all with their
arguments set up referencing the
titleObject
and
bodyObject
variables defined.
Testing the remove command
Let's test out the
remove
command in Terminal. I'll list out my notes using
node
flag
"t"
: node app.js remove -t="t"
, so I can see which notes I
have to remove:
I'll
the note with the
title
, using
the
command and
our
We'll remove the note with the title
t
, and as shown previous,
Note was removed
prints
to the screen. And if I use the up arrow key twice, I can list the notes out again,
and you can see the note with the title
t
has indeed gone:
Let's remove one more note using the
node app.js remove
command. This time we're
going to use
--title
, which is the argument name, and the note we're going to
remove
has the title flag title, as shown in this code:
When I remove it, it says
Note was removed
, and if I rerun the
list
command, I can see
that we have three notes left, the note was indeed removed , as shown here:
And that is it for the notes application.
Arrow functions
In this section, you're going to learn the ins and outs of the arrow function. It's an
ES6 feature, and we have taken a little look at it. Inside
notes.js
we used it in a few
basic examples to create methods such as
fetchNotes
and
saveNotes
, and we also passed
it into a few array methods like filter, and for each array, we used it as the callback
function that gets called once for every item in the array.
Now if you try to swap out all of the functions in a program with arrow functions,
it's most likely not going to work as expected because there are some differences
between the two, and it's really important to know what those differences are, so
you can make the decision to use a regular ES5 function or an ES6 arrow function.
Using the arrow function
The goal in this section is to give you the knowledge to make that choice, and
we'll kick things off by creating a new file in the playground folder called
arrow
function.js:
Inside this file, we're going to play around with a few examples, going over some
of the subtleties to the arrow function. Before we type anything inside of the file,
I'll start up this file with
nodemon
, so every time we make a change it automatically
refreshes over in Terminal.
If you remember,
nodemon
is the utility we installed in
Chapter 2
, Node Fundamentals
- Part 1. It was a global npm module. The
nodemon
is the command to run, and then
we just pass in the file path like we would for any other Node command. As we're
going into the
playground
folder, and the file itself is called arrow-function.js, we'll run the
following command: nodemon playground/arrow-function.js
We'll run the file, and nothing prints to the screen, as shown in the following
output, besides the
nodemon
logs because we have nothing in the file:
To get started, in the
arrowfunction.js
file, we'll create a function called square, by
making a variable called square and setting it equal to an arrow function.
To make our arrow function (
=>
), we'll first provide the arguments inside
parentheses. Since we'll be squaring a number, we just need one number, and I'll
refer to that number as
x
. If I pass in 3, I should expect 9 back, and if I pass in 9,
I would expect 81 back.
After the arguments list, we have to put the arrow in arrow function (
=>
) by putting
the equal sign and the greater than symbol together, creating our nice little arrow.
From here we can provide, inside curly braces, all the statements we want to
execute:
var square = (x) => {
};
Next, we might create a variable called result, setting that equal to
x
times
x
, then
we might return the result variable using the
return
keyword, as shown here:
var square = (x) => { var
result = x * x; return result;
};
Now, obviously this can be done on one line, but the goal here is to illustrate that
when you use the statement arrow function (
=>
), you can put as many lines as you
want in between those curly braces. Let's call a square, we'll do that using console.log
so we can print the result to the screen. I'll call square; and we'll call square with
9
, the square of
9
would be
81
, so we would expect
81
to print to the screen:
var square = (x) => { var result =
x * x; return result;
}; console.log(square(9));
I'll save the arrow function (
=>
) file, and in Terminal,
81
shows up just as we expect:
Now the syntax we used in the previous example is the statement syntax for the
arrow function (
=>
). We've also explored the expression syntax earlier, which lets
you simplify your arrow functions when you return some expressions. In this case
all we need to do is specify the expression we want to return. In our case that's
x
times
x
:
var square = (x) => x * x; console.log(square(9));
You don't need to explicitly add the
return
keyword. When you use an arrow
function (
=>
) without your curly braces, it's implicitly provided for you. That
means we can save the function as shown previous and the exact same result is
going to print to the screen,
81
shows up.
This is one of the great advantages of arrow functions when you use them in cases
like filter or for those which we did in the
notes.js
file. It lets you simplify your code
keeping everything on one line and making your code a lot easier to maintain and
scan.
Now, there is one thing I want to note: when you have an arrow
function (
=>
) that has just one argument, you can actually leave off
the parentheses. If you have two or more arguments, or you have
zero arguments, you are going to need to provide the parentheses,
but if you just have one argument, you can reference it with no
parentheses.
If I save the file in this state,
81
still prints to the screen; and this is great we have
an even simpler version of our arrow function (
=>
):
Now that we have a basic example down, I want to move on to a more complex
example that's going to explore the nuances between regular functions and arrow
functions.
Exploring the difference between
regular and arrow functions
To illustrate the difference, I'll make a variable called
user
, which will be an object.
On this object we'll specify one property, name. Set name equal to the string, your
name, in this case I'll set it equal to the string
Andrew
:
var user = { name:
'Andrew' };
Then we can define a method on the
user
object. Right after name, with my comma
at the end of the line, I'll provide the method
sayHi
, setting it equal to an arrow
function (
=>
) that doesn't take any arguments. For the moment, we'll keep the
arrow function really simple:
var user = { name:
'Andrew', sayHi: () =>
{
}
};
All we'll do inside
sayHi
is use
console.log
to print to the screen, inside of template
strings
Hi
:
var user = { name: 'Andrew',
sayHi: () => {
console.log(`Hi`);
}
};
We're not using template strings yet, but we will later so I'll use them here. Down
following the user object, we can test out
sayHi
by calling it,
user.sayHi
:
var user = { name: 'Andrew',
sayHi: () => {
console.log(`Hi`);
} }; user.sayHi();
I'll call it then save the file, and we would expect that
Hi
prints to the screen
because all our arrow function (
=>
) does is use
console.log
to print a static string.
Nothing in this case will cause any problems; you'd be able to swap out a regular
function for an arrow function (
=>
) without issue.
Now the first issue that will arise when you use arrow functions is the fact that
arrow functions do not bind a
this
keyword. So if you are using
this
inside your
function, it's not going to work when you swap it out for an arrow function (
=>
).
Now,
this
binding; refers to the parent binding, in our case there is no parent,
function so this would refer to the global
this
keyword. Now we have our console.log
that does not use
this
, I'll swap it out for a case that does.
We'll put a period after
Hi
, and I'll say I'm, followed by the name, which we would
usually be able to access via
this.name
:
var user = { name: 'Andrew', sayHi: () => {
console.log(`Hi. I'm ${this.name}`);
} }; user.sayHi();
If I try to run this code, it is not going to work as expected; we're going to get
Hi
I'm undefined printing to the screen, as shown here:
In order to fix this, we'll look at an alternative syntax to arrow functions that's
great when you're defining object literals, as we are in this case.
After
sayHi
, I'll make a new method called
sayHiAlt
using a different ES6 feature. ES6
provides us a new way to make methods on objects; you provide the method
name,
sayHiAlt
, then you go right to the parentheses skipping the colon. There's also
no need for the function keyword, even though it is a regular function it's not an
arrow function (
=>
). Then we move on to our curly braces as shown here:
var user = { name: 'Andrew', sayHi: () => {
console.log(`Hi. I'm ${this.name}`); }, sayHiAlt() {
} }; user.sayHi();
Inside here I can have the exact same code we have in the
sayHi
function, but
it is going to work as expected. It's going to print
Hi. I'm Andrew
. I'll call
sayHiAlt
down following instead of the regular
sayHi
method:
var user = { name: 'Andrew', sayHi: () => {
console.log(`Hi. I'm ${this.name}`);
}, sayHiAlt() { console.log(`Hi. I'm ${this.name}`);
} }; user.sayHiAlt();
And in Terminal, you can see
Hi. I'm Andrew
, prints to the screen:
The
sayHiAlt
syntax is a syntax that you can use to solve
this
problem when you create
functions on object literals. Now that we know that the
this
keyword does not get
bound, let's explore one other quirk that arrow functions have, it also does not
bind the arguments array.
Exploring the arguments array
Regular functions, like
sayHiAlt
, are going to have an arguments array that's
accessible inside of the function:
var user = { name: 'Andrew', sayHi: () => {
console.log(`Hi. I'm ${this.name}`);
}, sayHiAlt() {
console.log(arguments);
console.log(`Hi. I'm ${this.name}`);
} }; user.sayHiAlt();
Now, it's not an actual array, it's more like an object with array; like
properties, but the arguments object is indeed specified in a regular function.
If I pass in one, two, and three and save the file, we'll get that back when we log
out arguments:
var user = { name: 'Andrew', sayHi: () => {
console.log(`Hi. I'm ${this.name}`);
}, sayHiAlt() {
console.log(arguments);
console.log(`Hi. I'm ${this.name}`);
} }; user.sayHiAlt(1, 2, 3);
Inside
nodemon
, it's taking a quick second to restart, and right here we have our
object:
We have one, two, and three, we have the index for each as the property name,
and this works because we're using a regular function. If we were to switch to the
arrow function (
=>
) though, it is not going to work as expected.
I'll add
console.log(arguments)
inside of my arrow function (
=>
), and I'll switch from
calling
sayHiAlt
back to the original method
sayHi
, as shown here:
var user = { name: 'Andrew', sayHi:
() => { console.log(arguments);
console.log(`Hi. I'm ${this.name}`);
}, sayHiAlt() {
console.log(arguments);
console.log(`Hi. I'm ${this.name}`);
} }; user.sayHi(1, 2, 3);
When I save the file in
arrow-function.js
, we'll get something a lot different from
what we had before. What we'll actually get is the global arguments variable,
which is the arguments variable for that wrapper function we explored:
In the previous screenshot, we have things like the require function, definition,
our modules object, and a couple of string paths to the file and to the current
directory. These are obviously not what we're expecting, and that is another thing
that you have to be aware of when you're using arrow functions; you're not going
to get the
arguments
keyword, you're not going to get the
this
binding (defined in sayHi
syntax) that you'd expect.
These problems mostly arise when you try to create methods on an object and use
arrow functions. I would highly recommend that you switch to
sayHiAlt
syntax which
we discussed, in those cases. You get a simplified syntax, but you also get the
disk binding and you get your arguments variable as you'd expect.
Summary
In this chapter, we were able to reuse the utility functions that we already made
in previous chapters, making the process of filling out a remove note that much
easier. Inside
app.js
, we worked on how the
removeNote
function is executed, if it was
executed successfully, we print a message; if it didn't, we print a different
message.
Next, we were able to successfully fill out the
read
command and we also created
a really cool utility function that we can take advantage of in multiple places. This
keeps our code DRY and prevents us from having the same code in multiple
places inside of our application.
Then we discussed a quick introduction to debugging. Essentially, debugging is
a process that lets you stop the program at any point in time and play around with
the program as it exists at that moment. That means you can play around with
variables that exist, or functions, or anything inside of Node. We learned more
about yargs, its configuration, setting up commands, their description, and
arguments.
Last, you explored a little bit more about arrow functions, how they work, when
to use them, and when not to use them. In general, if you don't need this keyword,
or the arguments keyword you can use an arrow function without a problem, and
I always prefer using arrow functions over regular functions when I can.
In the next chapter, we will explore asynchronous programming and how we can
fetch data from third-party APIs. We'll use both regular functions and arrow
functions a lot more, and you'll be able to see firsthand how to choose between
one over the other.
Basics of Asynchronous
Programming in Node.js
If you've read any article about Node, you'd have probably come across four
terms: asynchronous, non-blocking, event-based, and single-threaded. All of
those are accurate terms to describe Node; the problem is it usually stops there,
and it's really abstract. The topic of asynchronous programming in Node.js has
been divided into three chapters. The goal in these upcoming three chapters is to
make asynchronous programming super practical by putting all these terms to use
in our weather application. That's the project we're going to be building in these
chapters.
This chapter is all about the basics of asynchronous programming. We'll look into
the basic concepts, terms, and technology related to async programming. We'll
look into making requests to Geolocation APIs. We'll need to make asynchronous
HTTP requests. Let's dive in, looking at the very basics of async programming in
Node.
Specifically, we'll look into the following topics:
The basic concept of asynchronous program
Call stack and event loop
Callback functions and APIs
HTTPS requests
The basic concept of
asynchronous program
In this section, we're going to create our first asynchronous non-blocking
program. This means our app will continue to run while it waits for something
else to happen. In this section, we'll look at a basic example; however, in the
chapter, we'll be building out a weather app that communicates with third-party
APIs, such as the Google API and a weather API. We'll need to use asynchronous
code to fetch data from these sources.
For this, all we need to do is make a new folder on the desktop for this chapter.
I'll navigate onto my desktop and use
mkdir
to make a new directory, and I'll call
this one
weather-app
. All I need to do is navigate into the weather app:
Now, I'll use the
clear
command to clear the Terminal output.
Now, we can open up that new
weather app
directory inside of Atom:
This is the directory we'll use throughout this entire chapter. In this section, we'll
not be building out the weather app just yet, we'll just play around with the async
features. So inside
weather-app
we'll make the
playground
folder.
This code is not going to be a part of the weather app, but it will be really useful
when it comes to creating the weather app in the later sections. Now inside
playground, we can make the file for this section. We'll name it
async-basics.js
as shown
here:
Illustrating
the
async
programming model
To illustrate how the asynchronous programming model works, we'll get started
with a simple example using
console.log
. Let's get started by adding a couple of
console.log statements in a synchronous way. We'll create one
console.log
statement at the
beginning of the app that will say
Starting app
, and we will add a second one to the
end, and the second one will print
Finishing up
, as shown here:
console.log('Starting app'); console.log('Finishing up');
Now these are always going to run synchronously. No matter how many times
you run the program,
Starting app
is always going to show up before
Finishing up
.
In order to add some asynchronous code, we'll take a look at a function that
Node provides called
setTimeout
. The
setTimeout
function is a great method for
illustrating the basics of non-blocking programming. It takes two arguments:
The first one is a function. This will be referred to as callback function, and
it will get fired after a certain amount of time.
The second argument is a number, which tells the number of milliseconds
you want to wait. So if you want to wait for one second, you would pass in
a thousand milliseconds.
Let's call
setTimeout
, passing in an arrow function (
=>
) as our first argument. This will
be callback function. It will get fired right away; that is, it will get fired after the
timeout is up, after our two seconds. And then we can set up our second argument
which is the delay,
2000
milliseconds, which equals those two seconds:
console.log('Starting app');
setTimeout(() => {
}, 2000);
Inside the arrow function (
=>
), all we'll do is use a
console.log
statement so that we
can figure out exactly when our function fires, because the statement will print to
the screen. We'll add
console.log
and then inside callback to get the job done, as
shown here:
setTimeout(() => { console.log('Inside of callback');
}, 2000);
With this in place, we're actually ready to run our very first async program, and
I'll not use
nodemon
to execute it. I'll run this file from the Terminal using the basic
Node command;
node playground
and the file inside the
playground
folder which is async-
basic.js: node playground/async-basics.js
Now pay close attention to exactly what happens when we hit enter. We'll see
two messages show up right away, then two seconds later our final message, Inside
of callback, prints to the screen:
The sequence in which these messages are shown is: first we got
Starting app
; almost
immediately after this,
Finishing up
prints to the screen and finally (two seconds later),
Inside of callback
was printed as shown in the previous code. Inside the file, this is not
the order in which we wrote the code, but it is the order the code executes in.
The
Starting app
statement prints to the screen as we expect. Next, we call setTimeout, but
we're not actually telling it to wait two seconds. We're registering a callback that
will get fired in two seconds. This will be an asynchronous callback, which means
that Node can do other things while these two seconds are happening. In this case,
the other thing it moves down to the
Finishing up
message. Now since we did register
this callback by using
setTimeout
, it will fire at some point in time, and two seconds
later we do see
Inside of callback
printing to the screen.
By using non-blocking I/O, we're able to wait, in this case two seconds, without
preventing the rest of the program from executing. If this was blocking I/O, we
would have to wait two seconds for this code to fire, then the
Finishing up
message
would print to the screen, and obviously that would not be ideal.
Now this is a pretty contrived example, we will not exactly use
setTimeout
in our
real-world apps to create unnecessary arbitrary delays, but the principles are the
same. For example, when we fetch data from the Google API we'll need to wait
about 100 to 200 milliseconds for that data to come back, and we don't want the
rest of the program to just be idle, it will continue. We'll register a callback, and
that callback will get fired once the data comes back from the Google servers.
The same principles applies even though what's actually happening is quite
different.
Now, we want to write another
setTimeout
right here. We want to register a setTimeout
function that prints a message; something like
Second setTimeout works
. This will be
inside the callback, and we want to register a delay of
0
milliseconds, no delay at
all. Let's fill out the async basics
setTimeout
. I'll call setTimeout with my arrow function
(
=>
), passing in a delay of
0
milliseconds, as shown in the following code. Inside
the arrow function (
=>
), I'll use
console.log
so I can see exactly when this function
executes, and I'll use
Second setTimeout
as the text:
setTimeout(() => { console.log('Second setTimeout');
}, 0);
Now that we have this in place, we can run the program from the Terminal, and
it's really important to pay attention to the order in which the statements print.
Let's run the program: node playground/async-basics.js
Right away we get three statements and then at the very end, two seconds later,
we get our final statement:
We start with
Starting app
, which makes sense, it's at the top. Then we get
Finishing up. After Finishing up we get Second setTimeout, which seems weird, because we clearly
told Node we want to run this function after
0
milliseconds, which should run it
right away. But in our example,
Second setTimeout
printed after Finishing up.
Finally,
Inside of callback
printed to the screen. This behavior is completely expected.
This is exactly how Node.js is supposed to operate, and it will become a lot clearer
after the next section, where we'll go through this example exactly, showing you
what happens behind the scenes. We'll get started with a more basic example
showing you how the call stack works, we'll talk all about that in the next section,
and then we'll go on to a more complex example that has some asynchronous
events attached to it. We'll discuss the reason why
Second setTimeout
comes up after the
Finishing up
message after the next section.
Call stack and event loop
In the last section, we ended up creating our very first asynchronous application,
but unfortunately we ended up asking more questions than we got answers. We
don't exactly know how async programming works even though we've used it.
Our goal for this section is to understand why the program runs the way it does.
For example, why does the two-second delay in the following code not prevent
the rest of the app from running, and why does a
0
second delay cause the function
to be executed after
Finishing up
prints to the screen?
console.log('Starting app');
setTimeout(() => { console.log('Inside of callback');
}, 2000);
setTimeout(() => { console.log('Second setTimeout');
}, 0); console.log('Finishing up');
These are all questions we'll answer in this section. This section will take you
behind the scenes into what happens in V8 and Node when an async program
runs. Now let's dive right into how the async program runs. We'll start with some
basic synchronous examples and then move on to figuring out exactly what
happens in the async program.
A synchronous program example
The following is example number one. On the left-hand side we have the code, a
basic synchronous example, and on the right-hand side we have everything that
happens behind the scenes, the Call Stack, our Node APIs, the Callback Queue,
and the Event Loop:
Now if you've ever read an article or watched any video lesson on how Node
works, you've most likely heard about one or more of these terms. In this section,
we'll be exploring how they all fit together to create a real-world, working Node
application. Now for our first synchronous example, all we need to worry about
is the Call Stack. The Call Stack is part of a V8, and for our synchronous example
it's the only thing that's going to run. We're not using any Node APIs and we're
not doing any asynchronous programming.
The call stack
The Call Stack is a really simple data structure that keeps track of program
execution inside of a V8. It keeps track of the functions currently executing and
the statements that are fired. The Call Stack is a really simple data structure that
can do two things:
You can add something on top of it
You can remove the top item
This means if there's an item at the bottom of the data structure and there's an item
above it, you can't remove the bottom item, you have to remove the top item. If
there's already two items and you want to add something on to it, it has to go on
because that's how the Call Stack works.
Think about it like a can of Pringles or a thing of tennis balls: if there's already an
item in there and you drop one in, the item you just dropped will not be the bottom
item, it's going to be the top item. Also, you can't remove the bottom tennis ball
from a can of tennis balls, you have to remove the one on top first.
That's exactly how the Call Stack works.
Running
the
synchronous
program
Now when we start executing the program shown in the following screenshot, the
first thing that will happen is Node will run the main function. The main function
is the wrapper function we saw over in nodemon (refer to, Installing the nodemon
module section in
Chapter 2
, Node Fundamentals Part-1) that gets wrapped around
all of our files when we run them through Node. In this case, by telling V8 to run
the main function we are starting the program.
As shown in the following screenshot, the first thing we do in the program is
create a variable
x
, setting it equal to
1
, and that's the first statement that's going
to run:
Notice it comes in on top of main. Now this statement is going to run, creating
the variable. Once it's done, we can remove it from the Call Stack and move on
to the next statement, where we make the variable
y
, which gets set equal to
x
,
which is
1
plus
9
. That means
y
is going to be equal to
10
:
As shown in the previous screenshot, we do that and move on to the next line.
The next line is our
console.log
statement. The
console.log
statement will print
y
is 10 to
the screen. We use template strings to inject the
y
variable: console.log(`y is ${y}`);
When we run this line it gets popped on to the Call Stack, as shown here:
Once the statement is done, it gets removed. At this point, we've executed all the
statements inside our program and the program is almost ready to be complete.
The main function is still running but since the function ends, it implicitly returns,
and when it returns, we remove main from the Call Stack and the program is
finished. At this point, our Node process is closed. Now this is a really basic
example of using the Call Stack. We went into the main function, and we moved
line by line through the program.
A complex synchronous program
example
Let's go over a slightly more complex example, our second example. As shown
in the following code, we start off by defining an
add
function. The
add
function
takes arguments
a
and
b
, adds them together storing that in a variable called
total
,
and returns
total
. Next, we add up
3
and
8
, which is
11
, storing it in the
res
variable.
Then, we print out the response using the
console.log
statement, as shown here:
var add = (a, b) => { var total = a
+ b;
return total;
}; var res = add(3, 8);
console.log(res);
That's it, nothing synchronous is happening. Once again we just need the Call
Stack. The first thing that happens is we execute the main function; this starts the
program we have here:
Then we run the first statement where we define the
add
variable. We're not
actually executing the function, we're simply defining it here:
In the preceding image, the
add()
variable gets added on to the Call Stack, and we
define
add
. The next line, line
7
, is where we call the
add
variable storing the return
value on the response variable:
When you call a function, it gets added on top of the Call Stack.
When you return from a function, it gets removed from the Call
Stack.
In this example, we'll call a function. So we're going to add
add()
on to the Call
Stack, and we'll start executing that function:
As we know, when we add main we start executing main and, when we add
add()
we start executing add. The first line inside
add
sets the
total
variable equal to
a +
b,
which would be
11
. We then return from the function using the
return total
statement.
That's the next statement, and when this runs,
add
gets removed:
So when
return total
finishes,
add()
gets removed, then we move on to the final line in
the program, our
console.log
statement, where we print
11
to the screen:
The
console.log
statement will run, print
11
to the screen and finish the execution, and
now we're at the end of the main function, which gets removed from the stack
when we implicitly return. This is the second example of a program running
through the V8 Call Stack.
An async program example
So far we haven't used Node APIs, the Callback Queue, or the Event Loop. The
next example will use all four (Call Stack, the Node APIs, the Callback Queue,
and the Event Loop). As shown on the left-hand side of the following screenshot,
we have our async example, exactly the same as we wrote it in the last section:
In this example, we will be using the Call Stack, the Node APIs, the Callback
Queue, and the Event Loop. All four of these are going to come into play for our
asynchronous program. Now things are going to start off as you might expect.
The first thing that happens is we run the main function by adding it on to the
Call Stack. This tells a V8 to kick off the code we have on the left side in the
previous screenshot, shown here again:
console.log('Starting app');
setTimeout(() => { console.log('Inside of callback');
}, 2000);
setTimeout(() => { console.log('Second setTimeout');
}, 0); console.log('Finishing up');
The first statement in this code is really simple, a
console.log
statement that prints
Starting app to the screen:
This statement runs right away and we move on to the second statement. The
second statement is where things start to get interesting, this is a call to
setTimeout
,
which is indeed a Node API. It's not available inside a V8, it's something that
Node gives us access to:
The Node API in async
programming
When we call the
setTimeout (2 sec)
function, we're actually registering the event
callback pair in the Node APIs. The event is simply to wait two seconds, and the
callback is the function we provided, the first argument. When we call
setTimeout
, it
gets registered right in the Node APIs as shown here:
Now this statement will finish up, the Call Stack will move on, and the
setTimeout
will start counting down. Just because the
setTimeout
is counting down, it doesn't
mean the Call Stack can't continue to do its job. The Call Stack can only run one
thing at a time, but we can have events waiting to get processed even when the
Call Stack is executing. Now the next statement that runs is the other call to
setTimeout:
In this, we register a
setTimeout
callback function with a delay of
0
milliseconds, and
the exact same thing happens. It's a Node API and it's going to get registered as
shown in the following screenshot. This essentially says that after zero seconds,
you can execute this callback:
The
setTimeout (0 sec)
statement gets registered and the Call Stack removes that
statement.
The callback queue in async
programming
At this point let's assume that
setTimeout
, the one that has a zero second delay,
finishes. When it finishes, it's not going to get executed right away; it's going to
take that callback and move it down into the Callback Queue, as shown here:
The Callback Queue is all the callback functions that are ready to get fired. In the
previous screenshot, we move the function from Node API into the Callback
Queue. Now the Callback Queue is where our callback functions will wait; they
need to wait for the Call Stack to be empty.
When the Call Stack is empty we can run the first function. There's another
function after it. We'll have to wait for that first function to run before the second
one does, and this is where the Event Loop comes into play.
The event loop
The Event Loop takes a look at the Call Stack. If the Call Stack is not empty, it
doesn't do anything because it can't, there is nothing it can do you can only run
one thing at a time. If the Call Stack is empty, the Event Loop says great let's see
if there's anything to run. In our case, there is a callback function, but because we
don't have an empty Call Stack, the Event Loop can't run it. So let's move on with
the example.
Running the async code
The next thing that happens in our program is we run our
console.log
statement, which
prints
Finishing up
to the screen. This is the second message that shows up in the
Terminal:
This statement runs, our main function is complete, and it gets removed from the
Call Stack.
At this point, the Event Loop says hey I see that we have nothing in the call stack
and we do have something in the Callback Queue, so let's run that callback
function. It will take the callback and move it into the Call Stack; this means the
function is executing:
It will run the first line which is sitting on line
8
,
console.log
, printing
Second
setTimeout to
the screen. This is why Second setTimeout shows up after Finishing up in our previous section
examples, because we can't run our callback until the Call Stack is complete.
Since
Finishing up
is part of the main function, it will always run before Second setTimeout.
After our
Second setTimeout
statement finishes, the function is going to implicitly return
and callback will get removed from the Call Stack:
At this point, there's nothing in the Call Stack and nothing in the Callback Queue,
but there is still something in our Node APIs, we still have an event listener
registered. So the Node process is not yet completed. Two seconds later, the
setTimeout(2 sec)
event is going to fire, and it's going to take that callback function and
move it into the Callback Queue. It gets removed from the Node APIs and it gets
added to the Callback Queue:
At this point, the Event Loop will take a look at the Call Stack and see it's empty.
Then it will take a quick look at the Callback Queue and see there is indeed
something to run. What will it do? It will take that callback, add it on to the Call
Stack, and start the process of executing it. This means that we'll run our one
statement inside callback. After that's finished, the callback function implicitly
returns and our program is complete:
This is exactly how our program ran. This illustrates how we're able to register
our events using Node APIs, and why when we use a
setTimeout
of zero the code
doesn't run right away. It needs to go through the Node APIs and through the
Callback Queue before it can ever execute on the Call Stack.
Now as I mentioned in the beginning of this section, the Call Stack, the Node
APIs, the Callback Queue, and the Event Loop are pretty confusing topics. A big
reason why they're confusing is because we never actually directly interact with
them; they're happening behind the scenes. We're not calling the Callback Queue,
we're not firing an Event Loop method to make these things work. This means
we're not aware they exist until someone explains them. These are topics that are
really hard to grasp the first time around. By writing real asynchronous code it's
going to become a lot clearer how it works.
Now that we got a little bit of an idea about how our code executes behind the
scenes, we'll move on with the rest of the chapter and start creating a weather app
that interacts with third-party APIs.
Callback functions and APIs
In this section, we'll take an in-depth look at callback functions, and use them to
fetch some data from a Google Geolocation API. That's going to be the API that
takes an address and returns the latitude and longitude coordinates, and this is
going to be great for the weather app. This is because the weather API we use
requires those coordinates and it returns the real-time weather data, such as the
temperature, five-day forecast, wind speed, humidity, and other pieces of weather
information.
The callback function
Before we get started making the HTTPS request, let's talk about callback
functions, and we have already used them. Refer to the following code (we used
it in the previous section):
console.log('Starting app');
setTimeout(() => { console.log('Inside of callback');
}, 2000);
setTimeout(() => { console.log('Second setTimeout');
}, 0); console.log('Finishing up');
Inside the
setTimeout
function we used a
callback
function. In general, a
callback
function is defined as a function that gets passed as an argument to another
function and is executed after some event happens. Now this is a general
definition, there is no strict definition in JavaScript, but it does satisfy the function
in this case:
setTimeout(() => { console.log('Inside of callback');
}, 2000);
Here we have a function and we pass it as an argument to another function,
setTimeout, and it does get executed after some event—two-second pass. Now the
event could be other things, it could be a database query finishes, it could be an
HTTP request comes back. In those cases, you will want a callback function, like
the one in our case, to do something with that data. In the case of
setTimeout
, we don't
get any data back because we're not requesting any; we're just creating an
arbitrary delay.
Creating the callback function
Now before we actually make an HTTP request to Google, let's create a callback
function example inside our
playground
folder. Let's make a new file called
callbacks.js:
Inside the file, we'll create a contrived example of what a callback function would
look like behind the scenes. We'll be making real examples throughout the book
and use many functions that require callbacks. But for this chapter, we'll start with
a simple example.
To get started, let's make a variable called
getUser
. This will be the function we'll
define that will show us exactly what happens behind the scenes when we pass a
callback to another function. The
getUser
callback will be something that simulates
what it would look like to fetch a user from a database or some sort of web API.
It will be a function, so we'll set it as such using arrow function (
=>
):
var getUser = () => {
};
The arrow function (
=>
) is going to take some arguments. The first argument it
will take is the
id
, which will be some sort of a unique number that represents each
user. I might have an
id
of
54
, you might have an
id
of
2000
; either way we're going
to need the
id
to find a user. Next up we'll get a callback function, which is what
we will call later with the data, with that user object:
var getUser = (id, callback) => {
};
This is exactly what happens when you pass a function to
setTimeout
.
The
setTimeout
function definition looks like this:
var getUser = (callback, delay) => {
};
It has a callback and a delay. You take the callback, and after a
certain amount of time passes, you call it. In our case, though, we'll
switch the order with an
id
first and the callback second.
Now we can call this function before actually filling it out. We'll call
getUser
, just
like we did with
setTimeout
in the previous code example. I'll call
getUser
, passing in
those two arguments. The first one will be some
id
; since we're faking it for now
it doesn't really matter, and I'll go with
31
. The second argument will be the
function that we want to run when the user data comes back, and this is really
important. As shown, we'll define that function:
getUser(31, () => {
});
Now the callback alone isn't really useful; being able to run this function after the
user data comes back only works if we actually get the user data, and that's what
we'll expect here:
getUser(31, (user) => {
});
We'll expect that the
user
objects, things like
id
,
name
,
email
,
password
, or whatever, comes
back as an argument to the callback function. Then inside the arrow function (
=>
),
we can actually do something with that data, for example, we could show it on a
web app, respond to an API request, or in our case we can simply print it to the
console,
console.log(user)
:
getUser(31, (user) => { console.log(user);
});
Now that we have the call in place, let's fill out the
getUser
function to work like we
have it defined.
The first thing I'll do is create a dummy object that's going to be the
user
object. In
the future, this is going to come from database queries, but for now we'll just
create a variable
user
setting it equal to some object:
var getUser = (id, callback) => { var user = {
}
};
Let's set an
id
property equal to whatever
id
the user passes in, and we'll set a
name
property equal to some name. I'll use
Vikram
:
var getUser = (id, callback) => { var user = {
id: id, name: 'Vikram'
};
};
Now that we have our
user
object, what we want to do is call the callback, passing
it as an argument. We'll then be able to actually run,
getUser(31, (user)
function, printing
the
user
to the screen. In order to do this, we would call the callback function like
any other function, simply referencing it by name and adding our parentheses like
this:
var getUser = (id, callback) => { var user = {
id: id, name: 'Vikram'
}; callback(); };
Now if we call the function like this, we're not passing any data from
getUser
back to the callback. In this case, we're expecting a
user
to get passed back, which
is why we are going to specify
user
as shown here:
callback(user);
Now the naming isn't important, I happen to call it
user
, but I could easily call this
userObject and userObject as shown here:
callback(user);
};
getUser(31, (userObject) => { console.log(userObject);
});
All that matters is the arguments, position. In this case, we call the first argument
userObject and the first argument pass back is indeed that
userObject
. With this in place
we can now run our example.
Running the callback function
In the Terminal, we'll run the callback function using
node
, which is in the playground
folder, and we call the file
callbacks.js
: node playground/callback.js
When we run the file, right away our data prints to the screen:
We've created a callback function using synchronous programming. Now as I
mentioned, this is still a contrived example because there is no need for a callback
in this case. We could simply return the user object, but in that case, we wouldn't
be using a callback, and the whole point here is to explore what happens behind
the scenes and how we actually call the function that gets passed in as an
argument.
Simulating
delay
using
setTimeout
Now, we can also simulate a delay using
setTimeout
, so let's do that. In our code, just
before the
callback (user)
statement, we'll use
setTimeout
just like we did before in the
previous section. We'll pass an arrow function (
=>
) in as the first argument, and
set a delay of 3 seconds using
3000
milliseconds:
setTimeout(() => {
}, 3000);
callback(user); };
Now I can take my callback call, delete it from line 10, and add it inside of the
callback function, as shown here:
setTimeout(() => { callback(user);
}, 3000);
};
Now we'll not be responding to the
getUser
request until three seconds have passed.
Now this will be more or less similar to what happens when we create real-world
examples of callbacks, we pass in a callback, some sort of delay happens whether
we're requesting from a database or from an HTTP endpoint, and then the
callback gets fired.
If I save
callbacks.js
and rerun the code from the Terminal, you'll see we wait those
three seconds, which is the simulated delay, and then the
user
object prints to the
screen:
This is exactly the principle that we need to understand in order to start working
with callbacks, and that is exactly what we'll start doing in this section.
Making request to Geolocation
API
The requests that we'll be making to that Geolocation API can actually be
simulated over in the browser before we ever make the request in Node, and that's
exactly what we want to do to get started. So follow along for the URL,
htt
ps://maps.googleapis.com/maps/api/geocode/json.
Now this is the actual endpoint URL, but we do have to specify the address for
which we want the geocode. We'll do that using query strings, which will be
provided right after the question mark. Then, we can set up a set of key value
pairs and we can add multiples using the ampersand in the URL, for example:
htt
ps://maps.googleapis.com/maps/api/geocode/json?key=value&keytwo=valuetwo.
In our case, all we need is one query string address, https://maps.googleapis.com/maps
/api/geocode/json?addres
s
, and for the address query string we'll set it equal to an address.
In order to fill out that query address, I'll start typing
1301 lombard street
philadelphia.
Notice that we are using spaces in the URL. This is just to illustrate a point: we
can use spaces in the browser because it's going to automatically convert those
spaces to something else. However, inside Node we'll have to take care of that
ourselves, and we'll talk about that a little later in the section. For now if we leave
the spaces in, hit enter, and we can see they automatically get converted for us:
Space characters get converted to
%20
, which is the encoded version of a space. In
this page, we have all of the data that comes back:
Now we'll use an extension called JSONView, which is available for Chrome and
Firefox.
I highly recommend installing JSONView, as we should see a much
nicer version of our JSON data. It lets us minimize and expand
various properties, and it makes it super easy to navigate.
Now as shown in the preceding screenshot, the data on this page has exactly what
we need. We have an address_components property, we don't need that. Next, we
have a formatted address which is really nice, it includes the state, the zip code,
and the country, which we didn't even provide in the address query.
Then, we have what we really came for: in geometry, we have location, and this
includes the latitude and longitude data.
Using Google Maps API data in
our code
Now, what we got back from the Google Maps API request is nothing more than
some JSON data, which means we can take that JSON data, convert it to a
JavaScript object, and start accessing these properties in our code. To do this,
we'll use a third-party module that lets us make these HTTP requests inside of
our app; this one is called request.
We can visit it by going to https://www.npmjs.com/package/request. When we visit this page,
we'll see all the documentation and all the different ways we can use the request
package to make our HTTP requests. For now, though, we'll stick to some basic
examples. On the request documentation page, on the right-hand side, we can see
this is a super popular package and it has seven hundred thousand downloads in
the last day:
In order to get started we're going to install the package inside our project, and
we'll make a request to this URL.
Installing the request package
To install the package, we'll go to the Terminal and install the module using
npm
init, to create the
package.json
file:
We'll run this command and use enter to use the defaults for every single option:
At the end, we'll type
yes
and hit enter again.
Now that we have our
package.json
file we can use
npm install
, followed by the module
name, request, and I will specify a version. You can always find the latest version
of modules on the npm page. The latest version at the time of writing is
2.73.0
, so
we'll add that,
@2.73.0
. Then we can specify the save flag because we do want to
save this module in our
package.json
file: npm install request@2.73.0 --save
It will be critical for running the weather application.
Using request as a function
Now that we have the request module installed, we can start using it. Inside Atom
we'll wrap up the section by making a request to that URL, in a new file in the
root of the project called
app.js
. This will be the starting point for the weather
application. The weather app will be the last command-line app we create. In the
future we'll be making the backend for web apps as well as real-time apps using
Socket.IO. But to illustrate asynchronous programming, a command-line app is
the nicest way to go.
Now, we have our app file, and we can get started by loading in
request
just like we
did with our other npm modules. We'll make a constant variable, call it request, and
set it equal to
require(request)
, as shown here: const request = require('request');
Now what we need to do is make a
request
. In order to do this, we'll have to call the
request
function. Let's call it, and this function takes two arguments:
The first argument will be an options object where we can configure all sorts
of information
The second one will be a callback function, which will be called once the data
comes back from the HTTP endpoint
request({}, () => {
});
In our case, it's going to get called once the JSON data, the data from the Google
Maps API, comes back into the Node application. We can add the arguments that
are going to get passed back from
request
. Now, these are arguments that are
outlined in the
request
documentation, I'm not making up the names for these:
In the documentation, you can see they call it error, response, and body. That's
exactly what well call ours. So, inside Atom, we can add
error
,
response
, and
body
, just
like the docs.
Now we can fill out that options object, which is where we are going to specify
the things unique to our
request
. In this case, one of the unique things is the URL.
The URL specifies exactly what you want to request, and in our case, we have
that in the browser. Let's copy the URL exactly as it appears, pasting it inside of
the string for the URL property:
request({ url: 'https://maps.googleapis.com/maps/api/geocode/json?address=1301%20lombard%20stre
}, (error, response, body) => {
});
Now that we have the URL property in place, we can add a comma at the very
end and hit enter. Because we will specify one more property, we'll set
json
equal
to true:
request({ url: 'https://maps.googleapis.com/maps/api/geocode/json?address=1301%20lombard%20stre json: true
}, (error, response, body) => {
});
This tells
request
that the data coming back is going to be JSON data, and it should
go ahead, take that JSON string, and convert it to an object for us. That lets us
skip a step, it's a really useful option.
With this in place, we can now do something in the callback. In the future we'll
be taking this longitude and latitude and fetching weather. For now, we'll simply
print the
body
to the screen by using
console.log
. We'll pass the body argument into
console.log, as shown here:
request({ url: 'https://maps.googleapis.com/maps/api/geocode/json?address=1301%20lombard%20stre json: true
}, (error, response, body) => { console.log(body);
});
Now that we have our very first HTTP request set up, and we have a callback
that's going to fire when the data comes back, we can run it from the Terminal.
Running the request
To run the request, we'll use
node
and run the
app.js
file: node
app.js
When we do this, the file will start executing and there will be a really short delay
before the body prints to the screen:
What we get back is exactly what we saw in the browser. Some of the properties,
such as
address_components
, show object in this case because we're printing it to the
screen. But those properties do indeed exist; we'll talk about how to get them later
in the chapter. For now, though, we do have our
formatted_address
as shown in the
preceding screenshot, the
geometry
object, the
place_id
, and
types
. This is what we'll be
using to fetch the longitude and latitude, and later to fetch the weather data.
Now that we have this in place, we are done. We have the first step of the process
complete. We've made a request to the Google Geolocation API, and we're getting
the data back. We'll continue creating the weather app in the next section.
Pretty printing objects
Before we continue learning about HTTP and what exactly is inside of
error
, response,
and
body
, let's take a quick moment to talk about how we can pretty print an object
to the screen. As we saw in the last subsection, when we ran our app with
node app.js
,
the body prints to the screen.
But since there is a lot of objects nested inside of each other, JavaScript starts
clipping them:
As shown in the preceding screenshot, it tells us an object is in the
results
, but we
don't get to see exactly what the properties are. This takes place for address_components,
geometry
, and
types
. Obviously this is not useful; what we want to do is see exactly
what's in the object.
Using the body argument
To explore all of the properties, we're going to look at a way to pretty print our
objects. This is going to require a really simple function call, a function we've
actually already used,
JSON.stringify
. This is the function that takes your JavaScript
objects, which
body
is, remember we used the
json: true
statement to tell
request
to take
the JSON and convert it into an object. In the
console.log
, statement we'll take that
object, pass
body
in, and provide the arguments as shown here:
const request = require('request');
request({ url: 'https://maps.googleapis.com/maps/api/geocode/json?address=1301%20lombard%20stre json: true
}, (error, response, body) => { console.log(JSON.stringify(body));
});
Now, this is how we've usually used
JSON.stringify
, in the past we provided just
one argument, the object we want to
stringify
, in this case we're going to provide a
couple of other arguments. The next argument is used to filter out properties. We
don't want to use that, it's usually useless, so we're going to leave it as undefined
as of now: console.log(JSON.stringify(body, undefined));
The reason we need to provide it, is because the third argument is the thing we
want. The third argument will format the JSON, and we'll specify exactly how
many spaces we want to use per indentation. We could go with
2
or
4
depending
on your preference. In this case, we'll pick
2
: console.log(JSON.stringify(body, undefined, 2));
We'll save the file and rerun it from the Terminal. When we
stringify
our JSON and
print it to the screen, as we'll see when we rerun the app, we get the entire object
showing up. None of the properties are clipped off, we can see the entire
address_components array, everything shows up no matter how complex it is:
Next, we have our geometry object, this is where our latitude and longitude are
stored, and you can see them as shown here:
Then below that, we have our
types
, which was cut off before, even though it was
an array with one item, which is a string:
Now that we know how to pretty print our objects, it will be a lot easier to scan
data inside of the console—none of our properties will get clipped, and it's
formatted in a way that makes the data a lot more readable. In the next section,
we'll start diving into HTTP and all of the arguments in our callback.
Making up of the HTTPS requests
The goal in the previous section was not to understand how HTTP works, or what
exactly the arguments,
error
,
response
, and
body
are the goal was to come up with a
real-world example of a callback, as opposed to the contrived examples that we've
been using so far with
setTimeout
:
const request = require('request');
request({ url: 'https://maps.googleapis.com/maps/api/geocode/json?address=1301%20lombard%20stree json: true
}, (error, response, body) => {
console.log(JSON.stringify(body, undefined, 2)); });
In the preceding case, we had a real callback that got fired once the HTTP
request came back from the Google servers. We were able to print the
body
, and
we saw exactly what we had in the website. In this section, we'll dive into these
arguments, so let's kick things off by taking a look at the
body
argument. This is
the third argument that
request
passes to the callback.
Now the
body
is not something unique to the
request
module (
body
is part of HTTP,
which stands for the Hypertext Transfer Protocol). When you make a request
to a website, the data that comes back is the body of the request. We've actually
used the body about a million times in our life. Every single time we request a
URL in the browser, what we get rendered inside the screen is the body.
In the case of
https://www.npmjs.com
, the body that comes back is an HTML web page
that the browser knows how to render. The body could also be some JSON
information, which is the case in our Google API request. Either way, the body
is the core data that comes back from the server. In our case, the body stores all
of the location information we need, and we'll be using that information to pull
out the formatted address, the latitude, and the longitude in this section.
The response object
Before we dive into the body, let's discuss about the
response
object. We can look at
the
response
object by printing it to the screen. Let's swap out body in the console.log
statement for
response
in the code:
const request = require('request'); request({ url:
'https://maps.googleapis.com/maps/api/geocode/json?address=1301%20lombard%20stre json: true
}, (error, response, body) => {
console.log(JSON.stringify(response, undefined, 2)); });
Then save the file and rerun things inside of the Terminal by running the
node
app.js command. We'll get that little delay while we wait for the request to come
back, and then we get a really complex object:
In the preceding screenshot, we can see the first thing we have in the
response
object
is a status code. The status code is something that comes back from an HTTP
request; it's a part of the response and tells you exactly how the request went.
In this case,
200
means everything went great, and you're probably familiar with
some status codes, like 404 which means the page was not found, or 500 which
means the server crashed. There are other body codes we'll be using throughout
the book.
We'll be making our very own HTTP API, so you'll become
intimately familiar with how to set and use status codes.
In this section, all we care about is that the status code is
200
, which means things
went well. Next up in the
response
object, we actually have the
body
repeated because
it is part of the
response
. Since it's the most useful piece of information that comes
back, the request module developers chose to make it the third argument,
although you could access it using
response.body
as you can clearly see in this case.
Here, we have all of the information we've already looked at, address
components, formatted address geometry, so on.
Next to the body argument, we have something called
headers
, as shown here:
Now,
headers
are part of the HTTP protocol, they are key-value pairs as you can see
in the preceding screenshot, where the key and the value are both strings. They
can be sent in the request, from the Node server to the Google API server, and in
the response from the Google API server back to the Node server.
Headers are great, there's a lot of built-in ones like
content-type
. The
content-type
is
HTML for a website, and in our case, it's
application/json
. We'll talk about headers
more in the later chapters. Most of these headers are not important to our
application, and most we're never ever going to use. When we go on and create
our own API later in the book, we'll be setting our own headers, so we'll be
intimately familiar with how these headers work. For now, we can ignore them
completely, all I want you to know is that these headers you see are set by Google,
they're headers that come back from their servers.
Next to the headers we have the request object, which stores some information
about the request that was made:
As shown in the preceding screenshot, you can see the protocol HTTPS, the host,
the
maps.googleapis.com
website, and other things such as the address parameters, the
entire URL, and everything else about the request, which is stored in this part.
Next, we also have our own headers. These are headers that were sent from Node
to the Google API:
This header got set when we added
json: true
to options object in our code. We told
request we want JSON back and request went on to tell Google, Hey, we want to
accept some JSON data back, so if you can work with that format send it back!
And that's exactly what Google did.
This is the
response
object, which stores information about the
response
and about the
request. While we'll not be using most of the things inside the
response
argument, it
is important to know they exist. So if you ever need to access them, you know
where they live. We'll use some of this information throughout the book, but as I
mentioned earlier, most of it is not necessary.
For the most part, we're going to be accessing the body argument. One thing we
will use is the status. In our case it was
200
. This will be important when we're
making sure that the request was fulfilled successfully. If we can't fetch the
location or if we get an error in the status code, we do not want to go on to try to
fetch the weather because obviously we don't have the latitude and longitude
information.
The error argument
For now, we can move on to the final thing which is error. As I just mentioned,
the status code can reveal that an error occurred, but this is going to be an error
on the Google servers. Maybe the Google servers have a syntax error and their
program is crashing, maybe the data that you sent is invalid, for example, you
sent an address that doesn't exist. These errors are going to become evident via
the status code.
What the error argument contains is errors related to the process of making that
HTTP request. For example, maybe the domain is wrong: if I delete
s
and the dot
with
go
in the URL, in our code, I get a URL that most likely doesn't exist:
const request = require('request');
request({ url: 'https://mapogleapis.com/maps/api/geocode/json?address=1301%20lombard%20street%2
In this case, I'll get an error in the error object because Node cannot make the
HTTP request, it can't even connect it to the server. I could also get an error if the
machine I'm making the request from does not have access to the internet. It's
going to try to reach out to the Google servers, it's going to fail, and we're going
to get an error.
Now, we can check out the error object by deleting those pieces of text from the
URL and making a request. In this case, I'll swap out response for
error
, as shown
here:
const request = require('request');
request({ url: 'https://mapogleapis.com/maps/api/geocode/json?address=1301%20lombard%20street%2 json: true
}, (error, response, body) => {
console.log(JSON.stringify(error, undefined, 2)); });
Now, inside the Terminal, let's rerun the application by running the
node app.js
command, and we can see exactly what we get back:
When we make the bad request, we get our error object printing to the screen, and
the thing we really care about is the error code. In this case we have the ENOTFOUND
error. This means that our local machine could not connect to the host provided.
In this case
mapogleapis.com
, it doesn't exist so we'll get an error right here.
These are going to be the system errors, things such as your program not being
able to connect to the internet or the domain not being found. This is also going
to be really important when it comes to creating some error handling for our
application there is a chance that the user's machine won't be connected to the
internet. We're going to want to make sure to take the appropriate action and we'll
do that depending on what is inside the error object.
If we can fix the URL, setting it back to
maps.googleapis.com
, and make the exact same
request by using the up arrow key and the enter key, the request error object it's
going to be empty, and you can see null print to the screen:
In this case, everything went great, there was no error, and it was able to
successfully fetch the data, which it should be able to because we have a valid
URL. That is a quick rundown of the body, the
response
, and the error argument.
We will use them in more detail as we add error handling.
Printing data from the body object
Now, we'll print some data from the body to the screen. Let's get started by
printing the formatted address, and then we will be responsible for printing both
the latitude and the longitude.
Printing the formatted address
We'll start with figure out where the formatted address is. For this, we'll go to the
browser and use JSONView. At the bottom of the browser page, you can see that
little blue bar shows up when we highlight over items, and it changes as we switch
items. For formatted address, for example, we access the
results
property, results is
an array. In the case of most addresses, you'll only get one result:
We'll use the first result every time, so we have the index of
0
, then it's the
.formatted_address property. This bottom line is exactly what we need to type inside of
our Node code.
Inside Atom, in our code, we'll delete the
console.log
statement, and replace it with a
new
console.log
statement. We'll use template strings to add some nice formatting to
this. We'll add
Address
with a colon and a space, then I'll inject the address using the
dollar sign and the curly braces. We'll access the body, results, and the first item
in the results array followed by formatted address, as shown here:
const request = require('request');
request({ url: 'https://maps.googleapis.com/maps/api/geocode/json?address=1301%20lombard%20stree json: true
}, (error, response, body) => {
console.log(`Address: ${body.results[0].formatted_address}`); });
With this in place, I can now add a semicolon at the end and save the file.
Next, we'll rerun the application inside of the Terminal, and this time around we
get our address printing to the screen, as shown here:
Now that we have the address printing to the screen, what we would like to print
both the latitude and the longitude next.
Printing latitude and longitude
In order to get started, inside Atom, we'll add another
console.log
line right next to
the
console.log
we added for formatted address. We'll use template strings again to
add some nice formatting. Let's print the latitude first.
For this, we'll add latitude followed by a colon. Then we can inject our variable
using the dollar sign with the curly braces. Then, the variable we want is on the
body. Just like the formatted address, it's also in the first results item; results at
the index of zero. Next, we'll be going into geometry. From geometry, we'll grab
the location property, the latitude,
.lat
, as shown here:
console.log(`Address: ${body.results[0].formatted_address}`); console.log(`Latitude:
${body.results[0].geometry.location.lat}`); });
Now that we have this in place, we'll do the exact same thing for longitude. We'll
add another
console.log
statement in the next line of the code. We'll use template
strings once again, typing longitude first. After that, we'll put a colon and then
inject the value. In this case, the value is on the body; it's in that same results item,
the first one. We'll go into geometry location again. Instead of
.lat
, we'll access
.lng
.
Then we can add a semicolon at the end and save the file. This will look
something like the following:
console.log(`Address: ${body.results[0].formatted_address}`); console.log(`Latitude:
${body.results[0].geometry.location.lat}`); console.log(`Longitude:
${body.results[0].geometry.location.lng}`); });
Now we'll test it from the Terminal. We'll rerun the previous command, and as
shown in the following screenshot, you can see we have the latitude,
39.94
, and the
longitude,
-75.16
printing to the screen:
And these are the exact same values we have inside the Chrome browser,
39.94
, -
75.16. With this in place, we've now successfully pulled off the data we need to
make that request to the weather API.
Summary
In this chapter, we have gone through a basic example of asynchronous
programming. Next, we talked about what happens behind the scenes when you
run asynchronous code. We got a really good idea about how your program runs
and what tools and tricks are happening behind the scenes to make it run the way
it does. We through a few examples that illustrate how the Call Stack, Node APIs,
the Callback Queue, and the Event Loop work.
Then, we learned how to use the request module to make an HTTP request for
some information, the URL we requested was a Google Maps Geocoding URL,
and we passed in the address we want the latitude and the longitude for. Then we
used a callback function that got fired once that data came back.
At the end of the section Callback functions and APIs, we looked into a quick tip
on how we can format objects when we want to print them to the console. Last,
we looked into what makes up the HTTPS request.
In the next chapter, we'll add some error handling to this callback because that's
going to be really important for our HTTP requests. There's a chance that things
will go wrong, and when they do, we'll want to handle that error by printing a
nice error message to the screen.
Callbacks in Asynchronous
Programming
This chapter is the second part of our asynchronous programming in Node.js. In
this chapter, we'll look at callbacks, HTTP requests, and more. We're going to
handle a lot of the errors that happen inside callbacks. There's a lot of ways our
request in
app.js
can go wrong, and we'll want to figure out how to recover from
errors inside of our callback functions when we're doing asynchronous
programming.
Next, we'll be moving our request code block into a separate file and abstracting
a lot of details. We'll talk about what that means and why it's important for us.
We'll be using Google's Geolocation API, and we'll be using the Dark Sky API
to take location information like a zip code and turn that into real-world current
weather information.
Then, we'll start wiring up that forecast API, fetching real-time weather data for
the address that's geocoded. We'll add our request inside of the callback for
geocodeAddress. This will let us take that dynamic set of latitude and longitude
coordinates, the
lat
/
lng
for the address used in the arguments list, and fetch the
weather for that location.
Specifically, we'll look into the following topics:
Encoding user input
Callback errors
Abstracting callbacks
Wiring up weather search
Chaining callbacks together
Encoding user input
In this section, you'll learn how to set up yargs for the weather app. You'll also
learn how to include user input, which is very important for our application.
As shown in the previous chapter, HTTPS request section, the user will not type
their encoded address into the Terminal; instead they will be typing in a plain text
address like 1301 Lombard Street.
Now this will not work for our URL, we need to encode those special characters,
like the space, replacing them with
%20
. Now
%20
is the special character for the
space, other special characters have different encoding values. We'll learn how to
encode and decode strings, so we can set up our URL to be dynamic. It's going to
be based off of the address provided in the Terminal. That's all we're going to
discuss in this section. By the end of the section, you'll be able to type in any
address you like, and you'll see the formatted address, the latitude, and the
longitude.
Installing yargs
Before we can get started doing any encoding, we have to get the address from
the user, and before we can set up yargs we have to install it. In the Terminal,
we'll run the
npm install
command, the module name is
yargs
, and we'll look for version
10.1.1, which is the latest version at the time of writing. We'll use the save flag to
run this installation, as shown in the following screenshot:
Now the
save
flag is great because as you remember. It updates the
package.json
file
and that's exactly what we want. This means that we can get rid of the node
modules folder which takes up a ton of space, but we can always regenerate it
using npm install.
If you run
npm install
without anything else, no other module names or
flags. It will dig through that
package.json
file looking for all the
modules to install, and it will install them, recreating your node
modules folder exactly as you left it.
While the installation is going on, we do a bit of configuration in the
app.js
file. So
we can get started by first loading in yargs. For this, in the
app.js
file, next to request
constant, I'll make a constant called
yargs
, setting it equal to
require(yargs)
just like this:
const request = require('request'); const yargs =
require('yargs');
Now we can go ahead and actually do that configuration. Next we'll make another
constant called
argv
. This will be the object that stores the final parsed output. That
will take the input from the process variable, pass it through
yargs
, and the result
will be right here in the
argv
constant. This will get set equal to yargs, and we can
start adding some calls:
const request = require('request'); const yargs =
require('yargs'); const argv = yargs
Now when we created the notes app we had various commands, you could add a
note and that required some arguments, list a note which required just the title,
list all notes which didn't require any arguments, and we specified all of that
inside of
yargs
.
For the weather app the configuration will be a lot simpler. There is no command,
the only command would be get weather, but if we only have one why even make
someone type it. In our case, when a user wants to fetch the weather all they will
do is type
node app.js
followed by the
address
flag just like this: node app.js --address
Then they can type their address inside of quotes. In my case it could be
something like 1301 lombard street: node app.js --address '1301 lombard street'
This is exactly how the command will get executed. There's no need for an actual
command like fetch weather, we go right from the file name right into our
arguments.
Configuring yargs
To configure yargs, things will look a little different but still pretty similar. In the
Atom, I'll get started by calling
.options
, which will let us configure some top level
options. In our case, we'll pass in an object where we configure all of the options
we need. Now I'll format this like I do for all of my chained calls, where I move
the call to the next line and I indent it like this:
const argv = yargs .options({
})
Now we can set up our options and in this case we just have one, it will be that
a
option;
a
will be short for address. I could either type address in the options and I
could put
a
in the alias, or I could put
a
in the options and type address in the alias.
In this case I'll put a as shown here:
const argv = yargs
.options({ a: {
}
})
Next up, I can go ahead and provide that empty object, and we'll go through these
same exact options we used inside of the notes app. We will demand it. If you'll
fetch the weather we need an address to fetch the weather for, so I'll set demand
equal to true:
const argv = yargs
.options({ a: {
demand: true,
}
})
Next up, we can set an
alias
, I'll set
alias
equal to
address
. Then finally we'll set describe,
we can set
describe
to anything we think would be useful, in this case I'll go with
Address to fetch weather for, as shown here:
const argv = yargs
.options({ a: {
demand: true, alias:
'address',
describe: 'Address to fetch weather for'
}
})
Now these are the three options we provided for the notes app, but I'll add a fourth
one to make our yargs configuration for the weather app even more full proof.
This will be an option called
string
. Now
string
takes a Boolean either
true
or
false
. In
our case we want
true
to be the value. This tells
yargs
to always parse the
a
or
address
argument as a string, as opposed to something else like a number or a Boolean:
const argv = yargs .options({ a: { demand: true, alias:
'address', describe: 'Address to fetch weather for', string: true
}
})
In the Terminal, if I were to delete the actual string
address
,
yargs
would still
accept this, it would just think I'm trying to add a Boolean flag, which could
be useful in some situations. For example, do I want to fetch in Celsius or in
Fahrenheit? But in our case, we don't need any sort of
true
or
false
flag, we need
some data, so we'll set string to true to make sure we get that data.
Now that we have our options configuration in place, we can go ahead and add a
couple other calls that we've explored. I'll add
.help
, calling it as shown in the
following code, which adds the
help
flag. This is really useful especially when
someone is first using a command. Then we can access
.argv
, which takes all of
this configuration, runs it through our arguments, and restores the result in the argv
variable:
const argv = yargs .options({ a: { demand: true, alias:
'address', describe: 'Address to fetch weather for', string: true
}
})
.help()
.argv;
Now the
help
method adds that
help
argument, we can also add an alias for it
right afterwards by calling
.alias
. Now
.alias
takes two arguments, the actual
argument that you want to set an alias for and the alias. In our case, we already
have
help
registered, it gets registered when we call
help
, and we'll set an alias which
will just be the letter
h
, awesome:
.help()
.alias('help', 'h')
.argv;
Now we have all sorts of really great configurations set up for the weather app.
For example, inside the Terminal I can now run
help
, and I can see all of the help
information for this application:
I could also use the shortcut
-h
, and I get the exact same data back:
Printing the address to screen
Now the address is also getting passed through but we don't print it to the screen,
so let's do that. Right after the configuration, let's use
console.log
to print the entire
argv
variable to the screen. This will include everything that got parsed by
yargs:
.help()
.alias('help', 'h')
.argv; console.log(argv);
Let's go ahead and rerun it in the Terminal, this time passing in an address. I'll
use the
a
flag, and specifying something like
1301 lombard street
, closing the quotes, and
hitting enter: node app.js -a '1301 lombard street'
When we do this we get our object, and as shown in the code output, we have
1301 Lombard St, Philadelphia, PA 19147, USA, the plain text address:
In the preceding screenshot, notice that we happen to fetch the latitude and
longitude for that address, but that's just because we have it hard coded in the
URL in
app.js
. We still need to make some changes in order to get the address, the
one that got typed inside the argument, to be the address that shows up in the
URL.
Encoding and decoding the
strings
To explore how to encode and decode strings we'll head into the Terminal. Inside
the Terminal, first we'll clear the screen using the
clear
command, and then we boot
up a node process by typing the
node
command as shown: node
Here we can run any statements we like. When we're exploring a really basic node
or JavaScript feature, we'll look into some examples first, and then we go ahead
and add it into our actual application. We'll look at two functions, encodeURIComponent
and
decodeURIComponent
. We'll get started with encoding first.
Encoding URI component
Encoding, the method is called
encodeURIComponent
, encode URI in uppercase
component, and it takes just one argument, the string you want to encode. In our
case, that string will be the address, something like 1301 lombard street philadelphia. When
we run this address through
encodeURIComponent
by hitting enter, we get the encoded
version back: encodeURIComponent('1301 lombard street philadelphia')
As shown in the following code output, we can see all the spaces, like the space
between 1301 and lombard, have been replaced with their encoded character, and
for the case of the space it is %20. By passing our string through encodeURIComponent,
we'll create something that's ready to get injected right into the URL so we can
fire off that dynamic request.
Decoding URI component
Now the alternative to
encodeURIComponent
is. This will take an encoded string like the
one in the previous example, and take all the special characters, like
%20
, and
convert them back into their original values, in this case space. For this, inside of
decodeURIComponent once again we'll pass a string.
Let's go ahead and type our first and last name. In my case it's
Andrew
, and instead
of a space between them I'll add
%20
, which we know is the encoded character for
a space. Since we're trying to decode something, it's important to have some
encoded characters here. Once yours looks like the following code with your first
and last name, you can go ahead and hit enter, and what we get back is the
decoded version: decodeURIComponent('Andrew%20Mead')
As shown in the following code output, I have Andrew Mead with the
%20
being
replaced by the space, exactly what we expected. This is how we can encode and
decode URI components in our app:
Pulling the address out of argv
Now what we want to do is pull the address out of
argv
, we already saw that it's
there, we want to encode it and we want to inject it in our URL in
app.js
file,
replacing the address:
This will essentially create that dynamic request we've been talking about. We'll
be able to type in any address we want, whether it's an address or a zip code or a
city state combination, and we'll be able to fetch the formatted address, the
latitude, and the longitude.
In order to get started, the first thing I'll do is get the encoded address. Let's make
a variable called
encodedAddress
in the
app.js
next to the
argv
variable, where we can store
that result. We'll set this equal to the return value from the method we just
explored in the Terminal,
encodeURIComponent
. This will take the plain text address and
return the encoded result.
Now we do need to pass in the string, and we have that available on
argv.address
which is the alias:
.alias('help', 'h') var encodedAddress = encodeURIComponent(argv.address);
Here we could use
argv.a
as well as
argv.address
, both will work the same.
Now we have that encoded result all that's left to do is inject it inside of the URL
string. In the
app.js
, currently we're using a regular string. We'll swap this out for a
template string so I can inject a variable inside of it.
Now that we have a template string, we can highlight the static address which
ends at
philadelphia
and goes up to the
=
sign, and remove it, and instead of typing in
a static address we can inject the dynamic variable. Inside of my curly braces,
encodedAddress, as shown here:
var encodedAddress = encodeURIComponent(argv.address);
request({ url: `https://maps.googleapis.com/maps/api/geocode/json?address=${encodedAddress}`,
With this in place we are now done. We get the address from the Terminal, we
encode it, and we use that inside of a
geocode
call. So the formatted address, latitude,
and longitude should match up. Inside the Terminal, we'll shut down node by
using control + C twice and use clear to clear the Terminal output.
Then we can go ahead and run our app using
node app.js
, passing in either the
a
or
address flag. In this case, we'll just use
a
. Then we can go ahead and type in an
address, for example, 1614 south broad street philadelphia as shown here: node app.js -a
'1614 south broad street philadelphia'
When you run it you should have that small delay while we fetch the
data from the geocode URL.
In this case we'll find that it's actually taking a little longer than we would expect,
about three or four seconds, but we do get the address back:
Here we have the formatted address with a proper zip code state and country, and
we also have the latitude and longitude showing up. We'll try a few other
examples. For example for a town in Pennsylvania called Chalfont, we can type
in
chalfont pa
which is not a complete address, but the Google Geocode API will
convert it into the closest thing, as shown here:
We can see that it's essentially the address of the town, Chalfont, PA 18914 is the
zip, with the state USA. Next, we have the general latitude and longitude data for
that town, and this will be fine for fetching weather data. The weather isn't exactly
changing when you move a few blocks over.
Now that we have our data coming in dynamically, we are able to move on to the
next section where we'll handle a lot of the errors that happen inside of callbacks.
There are a lot of ways this request can go wrong, and we'll want to figure out
how to recover from errors inside of our callback functions when we're doing
asynchronous programming.
Callback errors
In this section we'll learn how to handle errors inside of your callback functions,
because as you might guess things don't always go as planned. For example, the
current version of our app has a few really big flaws, if I try to fetch weather using
node app.js
with the
a
flag for a zip that doesn't exist, like
000000
, the program crashes,
which is a really big problem. It's going off. It's fetching the data, eventually that
data will come back and we get an error, as shown here:
It's trying to fetch properties that don't exist, such as body.results[0].formatted_address is not
a real property, and this is a big problem.
Our current callback expects everything went as planned. It doesn't care about the
error object, doesn't look at response codes; it just starts printing the data that it
wants. This is the happy path, but in real world node apps we have to handle
errors as well otherwise the applications will become really useless, and a user
can get super frustrated when things don't seem to be working as expected.
In order to do this, we'll add a set of
if
/
else
statements inside of the callback. This
will let us check certain properties to determine whether or not this call, the one
to our URL in the
app.js
, should be considered a success or a failure. For example,
if the response code is a 404, we might want to consider that a failure and we'll
want to do something other than trying to print the address, latitude and longitude.
If everything went well though, this is a perfectly reasonable thing to do.
There are two types of errors that we'll worry about in this section. That will be:
The machine errors, things like being unable to connect to a network, these
are usually will show up in the error object, and
The errors coming from the other server, the Google server, this could be
something like an invalid address
In order to get started, let's take a look at what can happen when we pass a bad
data to the Google API.